From d7f52a58f0266d859a234d08ddd4ce3bbbff84d0 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Fri, 28 Jul 2023 19:44:49 -0300 Subject: [PATCH] MAUI Single project migration initial --- global.json | 5 + src/App/App.csproj | 31 +- src/App/App.xaml | 4 +- src/App/App.xaml.cs | 16 +- ...entAutoBottomScrollingOnFocusedBehavior.cs | 4 +- .../AccountSwitchingOverlayView.xaml | 6 +- .../AccountSwitchingOverlayView.xaml.cs | 6 +- .../AccountSwitchingOverlayViewModel.cs | 3 +- .../AccountViewCell/AccountViewCell.xaml | 6 +- .../AccountViewCell/AccountViewCell.xaml.cs | 3 +- .../AuthenticatorViewCell.xaml | 2 +- .../AuthenticatorViewCell.xaml.cs | 3 +- src/App/Controls/AvatarImageSource.cs | 7 +- .../CipherViewCell/CipherViewCell.xaml | 4 +- .../CipherViewCell/CipherViewCell.xaml.cs | 3 +- src/App/Controls/CircularProgressbarView.cs | 14 +- src/App/Controls/CustomLabel.cs | 3 +- src/App/Controls/DateTime/DateTimePicker.xaml | 4 +- .../Controls/DateTime/DateTimePicker.xaml.cs | 10 +- src/App/Controls/ExtendedCollectionView.cs | 5 +- src/App/Controls/ExtendedDatePicker.cs | 3 +- src/App/Controls/ExtendedGrid.cs | 3 +- src/App/Controls/ExtendedSearchBar.cs | 5 +- src/App/Controls/ExtendedSlider.cs | 5 +- src/App/Controls/ExtendedStackLayout.cs | 3 +- src/App/Controls/ExtendedStepper.cs | 8 +- src/App/Controls/ExtendedTimePicker.cs | 3 +- src/App/Controls/ExtendedToolbarItem.cs | 3 +- src/App/Controls/HybridWebView.cs | 3 +- src/App/Controls/IconButton.cs | 4 +- src/App/Controls/IconLabel.cs | 4 +- .../IconLabelButton/IconLabelButton.xaml | 4 +- .../IconLabelButton/IconLabelButton.xaml.cs | 12 +- src/App/Controls/MiButton.cs | 4 +- src/App/Controls/MiLabel.cs | 4 +- src/App/Controls/MonoEntry.cs | 4 +- src/App/Controls/MonoLabel.cs | 4 +- .../PasswordStrengthProgressBar.xaml | 4 +- .../PasswordStrengthProgressBar.xaml.cs | 6 +- .../PasswordStrengthViewModel.cs | 3 +- src/App/Controls/RepeaterView.cs | 3 +- src/App/Controls/SelectableLabel.cs | 3 +- .../Controls/SendViewCell/SendViewCell.xaml | 4 +- .../SendViewCell/SendViewCell.xaml.cs | 3 +- src/App/Effects/FabShadowEffect.cs | 3 +- src/App/Effects/FixedSizeEffect.cs | 3 +- src/App/Effects/NoEmojiKeyboardEffect.cs | 3 +- src/App/Effects/RemoveFontPaddingEffect.cs | 3 +- src/App/Effects/ScrollEnabledEffect.cs | 3 +- ...iewContentInsetAdjustmentBehaviorEffect.cs | 3 +- src/App/Effects/TabBarEffect.cs | 3 +- .../CustomFieldItemTemplateSelector.cs | 3 +- .../BooleanCustomFieldItemLayout.xaml | 4 +- .../BooleanCustomFieldItemLayout.xaml.cs | 3 +- .../HiddenCustomFieldItemLayout.xaml | 4 +- .../HiddenCustomFieldItemLayout.xaml.cs | 3 +- .../LinkedCustomFieldItemLayout.xaml | 4 +- .../LinkedCustomFieldItemLayout.xaml.cs | 3 +- .../TextCustomFieldItemLayout.xaml | 4 +- .../TextCustomFieldItemLayout.xaml.cs | 3 +- .../BaseCustomFieldItemViewModel.cs | 3 +- .../HiddenCustomFieldItemViewModel.cs | 3 +- .../TextCustomFieldItemViewModel.cs | 3 +- .../Accounts/BaseChangePasswordViewModel.cs | 1 - src/App/Pages/Accounts/DeleteAccountPage.xaml | 4 +- .../Pages/Accounts/DeleteAccountPage.xaml.cs | 3 +- .../Pages/Accounts/DeleteAccountViewModel.cs | 2 +- src/App/Pages/Accounts/EnvironmentPage.xaml | 4 +- .../Pages/Accounts/EnvironmentPage.xaml.cs | 4 +- src/App/Pages/Accounts/HintPage.xaml | 4 +- src/App/Pages/Accounts/HintPage.xaml.cs | 4 +- src/App/Pages/Accounts/HintPageViewModel.cs | 2 +- src/App/Pages/Accounts/HomePage.xaml | 4 +- src/App/Pages/Accounts/HomePage.xaml.cs | 3 +- src/App/Pages/Accounts/HomePageViewModel.cs | 8 +- src/App/Pages/Accounts/LockPage.xaml | 4 +- src/App/Pages/Accounts/LockPage.xaml.cs | 4 +- src/App/Pages/Accounts/LockPageViewModel.cs | 6 +- src/App/Pages/Accounts/LoginPage.xaml | 4 +- src/App/Pages/Accounts/LoginPage.xaml.cs | 5 +- src/App/Pages/Accounts/LoginPageViewModel.cs | 5 +- .../Pages/Accounts/LoginPasswordlessPage.xaml | 4 +- .../Accounts/LoginPasswordlessPage.xaml.cs | 3 +- .../LoginPasswordlessRequestPage.xaml | 4 +- .../LoginPasswordlessRequestPage.xaml.cs | 3 +- .../LoginPasswordlessRequestViewModel.cs | 6 +- .../Accounts/LoginPasswordlessViewModel.cs | 3 +- src/App/Pages/Accounts/LoginSsoPage.xaml | 4 +- src/App/Pages/Accounts/LoginSsoPage.xaml.cs | 4 +- .../Pages/Accounts/LoginSsoPageViewModel.cs | 1 - src/App/Pages/Accounts/RegisterPage.xaml | 4 +- src/App/Pages/Accounts/RegisterPage.xaml.cs | 4 +- .../Pages/Accounts/RegisterPageViewModel.cs | 5 +- .../Accounts/RemoveMasterPasswordPage.xaml | 4 +- .../Accounts/RemoveMasterPasswordPage.xaml.cs | 3 +- src/App/Pages/Accounts/SetPasswordPage.xaml | 4 +- .../Pages/Accounts/SetPasswordPage.xaml.cs | 4 +- .../Accounts/SetPasswordPageViewModel.cs | 4 +- src/App/Pages/Accounts/TwoFactorPage.xaml | 4 +- src/App/Pages/Accounts/TwoFactorPage.xaml.cs | 5 +- .../Pages/Accounts/TwoFactorPageViewModel.cs | 14 +- .../Accounts/UpdateTempPasswordPage.xaml | 2 +- .../Accounts/UpdateTempPasswordPage.xaml.cs | 3 +- .../UpdateTempPasswordPageViewModel.cs | 3 +- .../Pages/Accounts/VerificationCodePage.xaml | 4 +- .../Accounts/VerificationCodeViewModel.cs | 7 +- src/App/Pages/BaseContentPage.cs | 9 +- src/App/Pages/BaseViewModel.cs | 5 +- src/App/Pages/CaptchaProtectedViewModel.cs | 1 - .../Pages/Generator/GeneratorHistoryPage.xaml | 4 +- .../Generator/GeneratorHistoryPage.xaml.cs | 4 +- .../GeneratorHistoryPageViewModel.cs | 3 +- src/App/Pages/Generator/GeneratorPage.xaml | 6 +- src/App/Pages/Generator/GeneratorPage.xaml.cs | 13 +- .../Pages/Generator/GeneratorPageViewModel.cs | 3 +- src/App/Pages/Send/SendAddEditPage.xaml | 6 +- src/App/Pages/Send/SendAddEditPage.xaml.cs | 11 +- .../Pages/Send/SendAddEditPageViewModel.cs | 7 +- .../Pages/Send/SendAddOnlyOptionsView.xaml | 6 +- .../Pages/Send/SendAddOnlyOptionsView.xaml.cs | 10 +- src/App/Pages/Send/SendAddOnlyPage.xaml | 4 +- src/App/Pages/Send/SendAddOnlyPage.xaml.cs | 3 +- .../SendGroupingsPage/SendGroupingsPage.xaml | 4 +- .../SendGroupingsPage.xaml.cs | 4 +- .../SendGroupingsPageListItemSelector.cs | 3 +- .../SendGroupingsPageViewModel.cs | 9 +- src/App/Pages/Send/SendsPage.xaml | 4 +- src/App/Pages/Send/SendsPage.xaml.cs | 4 +- src/App/Pages/Send/SendsPageViewModel.cs | 3 +- src/App/Pages/Settings/AutofillPage.xaml | 4 +- .../Pages/Settings/AutofillServicesPage.xaml | 4 +- .../Settings/AutofillServicesPage.xaml.cs | 4 +- .../Pages/Settings/BlockAutofillUrisPage.xaml | 4 +- .../Settings/BlockAutofillUrisPage.xaml.cs | 4 +- .../BlockAutofillUrisPageViewModel.cs | 4 +- src/App/Pages/Settings/ExportVaultPage.xaml | 4 +- .../Pages/Settings/ExportVaultPage.xaml.cs | 3 +- .../Settings/ExportVaultPageViewModel.cs | 3 +- src/App/Pages/Settings/ExtensionPage.xaml | 4 +- src/App/Pages/Settings/FolderAddEditPage.xaml | 4 +- .../Pages/Settings/FolderAddEditPage.xaml.cs | 6 +- .../Settings/FolderAddEditPageViewModel.cs | 7 +- src/App/Pages/Settings/FoldersPage.xaml | 4 +- src/App/Pages/Settings/FoldersPage.xaml.cs | 4 +- .../LoginPasswordlessRequestsListPage.xaml | 6 +- .../LoginPasswordlessRequestsListPage.xaml.cs | 3 +- .../LoginPasswordlessRequestsListViewModel.cs | 3 +- src/App/Pages/Settings/OptionsPage.xaml | 4 +- src/App/Pages/Settings/OptionsPage.xaml.cs | 8 +- .../Pages/Settings/OptionsPageViewModel.cs | 4 +- .../Settings/SettingsPage/SettingsPage.xaml | 4 +- .../SettingsPage/SettingsPage.xaml.cs | 4 +- .../SettingsPage/SettingsPageListItem.cs | 3 +- .../SettingsPageListItemSelector.cs | 3 +- .../SettingsPage/SettingsPageViewModel.cs | 15 +- src/App/Pages/Settings/SyncPage.xaml | 4 +- src/App/Pages/Settings/SyncPage.xaml.cs | 4 +- src/App/Pages/Settings/SyncPageViewModel.cs | 2 +- src/App/Pages/TabsPage.cs | 6 +- src/App/Pages/Vault/AttachmentsPage.xaml | 4 +- src/App/Pages/Vault/AttachmentsPage.xaml.cs | 5 +- .../Pages/Vault/AttachmentsPageViewModel.cs | 10 +- .../Vault/AutofillCiphersPageViewModel.cs | 5 +- src/App/Pages/Vault/CipherAddEditPage.xaml | 4 +- src/App/Pages/Vault/CipherAddEditPage.xaml.cs | 30 +- .../Pages/Vault/CipherAddEditPageViewModel.cs | 7 +- src/App/Pages/Vault/CipherDetailsPage.xaml | 4 +- src/App/Pages/Vault/CipherDetailsPage.xaml.cs | 5 +- .../Pages/Vault/CipherDetailsPageViewModel.cs | 11 +- src/App/Pages/Vault/CipherSelectionPage.xaml | 6 +- .../Pages/Vault/CipherSelectionPage.xaml.cs | 5 +- .../Vault/CipherSelectionPageViewModel.cs | 6 +- src/App/Pages/Vault/CiphersPage.xaml | 4 +- src/App/Pages/Vault/CiphersPage.xaml.cs | 4 +- src/App/Pages/Vault/CiphersPageViewModel.cs | 5 +- src/App/Pages/Vault/CollectionsPage.xaml | 4 +- src/App/Pages/Vault/CollectionsPage.xaml.cs | 4 +- .../Pages/Vault/CollectionsPageViewModel.cs | 2 +- .../Vault/GroupingsPage/GroupingsPage.xaml | 6 +- .../Vault/GroupingsPage/GroupingsPage.xaml.cs | 6 +- .../GroupingsPageListItemSelector.cs | 3 +- .../GroupingsPageTOTPListItem.cs | 4 +- .../GroupingsPage/GroupingsPageViewModel.cs | 10 +- .../Vault/OTPCipherSelectionPageViewModel.cs | 3 +- src/App/Pages/Vault/PasswordHistoryPage.xaml | 4 +- .../Pages/Vault/PasswordHistoryPage.xaml.cs | 4 +- .../Vault/PasswordHistoryPageViewModel.cs | 3 +- src/App/Pages/Vault/ScanPage.xaml | 4 +- src/App/Pages/Vault/ScanPage.xaml.cs | 8 +- src/App/Pages/Vault/ScanPageViewModel.cs | 6 +- src/App/Pages/Vault/SharePage.xaml | 4 +- src/App/Pages/Vault/SharePage.xaml.cs | 8 +- src/App/Pages/Vault/SharePageViewModel.cs | 2 +- .../Services/MobilePlatformUtilsService.cs | 5 +- src/App/Services/PreferencesStorageService.cs | 34 +- .../PushNotificationListenerService.cs | 3 +- src/App/Services/SecureStorageService.cs | 8 +- src/App/Styles/Android.xaml | 4 +- src/App/Styles/Android.xaml.cs | 3 +- src/App/Styles/Base.xaml | 4 +- src/App/Styles/Base.xaml.cs | 3 +- src/App/Styles/Black.xaml | 4 +- src/App/Styles/Black.xaml.cs | 3 +- src/App/Styles/Dark.xaml | 4 +- src/App/Styles/Dark.xaml.cs | 3 +- src/App/Styles/Light.xaml | 4 +- src/App/Styles/Light.xaml.cs | 3 +- src/App/Styles/Nord.xaml | 4 +- src/App/Styles/Nord.xaml.cs | 3 +- src/App/Styles/SolarizedDark.xaml | 4 +- src/App/Styles/SolarizedDark.xaml.cs | 3 +- src/App/Styles/Variables.xaml | 4 +- src/App/Styles/Variables.xaml.cs | 3 +- src/App/Styles/iOS.xaml | 4 +- src/App/Styles/iOS.xaml.cs | 3 +- .../AccountManagement/AccountsManager.cs | 4 +- src/App/Utilities/AppHelpers.cs | 4 +- .../BoxRowVsBoxRowInputPaddingConverter.cs | 3 +- src/App/Utilities/ColoredPasswordConverter.cs | 3 +- src/App/Utilities/DateTimeConverter.cs | 3 +- src/App/Utilities/EnumHelper.cs | 2 +- src/App/Utilities/GeneratedValueFormatter.cs | 9 +- src/App/Utilities/I18nExtension.cs | 5 +- src/App/Utilities/IconGlyphConverter.cs | 3 +- src/App/Utilities/IconImageConverter.cs | 3 +- src/App/Utilities/InverseBoolConverter.cs | 3 +- src/App/Utilities/IsNotNullConverter.cs | 3 +- src/App/Utilities/IsNullConverter.cs | 3 +- src/App/Utilities/LocalizableEnumConverter.cs | 3 +- src/App/Utilities/PageExtensions.cs | 3 +- src/App/Utilities/PermissionManager.cs | 3 +- src/App/Utilities/ProgressBarExtensions.cs | 3 +- src/App/Utilities/SendIconGlyphConverter.cs | 3 +- src/App/Utilities/StringHasValueConverter.cs | 3 +- src/App/Utilities/ThemeManager.cs | 11 +- src/App/Utilities/TimerTask.cs | 3 +- src/App/Utilities/UpperCaseConverter.cs | 3 +- .../VerificationActionsFlowHelper.cs | 3 +- src/Core/Core.csproj | 13 +- .../Abstractions/IAccountsManager.cs | 15 + .../Abstractions/IAccountsManagerHost.cs | 14 + .../Abstractions/IDeepLinkContext.cs | 9 + .../Abstractions/IDeviceActionService.cs | 45 + .../Abstractions/ILocalizeService.cs | 22 + .../Abstractions/IPasswordRepromptService.cs | 15 + .../IPushNotificationListenerService.cs | 17 + .../Abstractions/IPushNotificationService.cs | 17 + src/Maui/Bitwarden/App.xaml | 15 + src/Maui/Bitwarden/App.xaml.cs | 546 + ...entAutoBottomScrollingOnFocusedBehavior.cs | 43 + src/Maui/Bitwarden/Bitwarden.csproj | 357 + src/Maui/Bitwarden/Bitwarden.sln | 28 + .../AccountSwitchingOverlayView.xaml | 59 + .../AccountSwitchingOverlayView.xaml.cs | 196 + .../AccountSwitchingOverlayViewModel.cs | 89 + .../AccountViewCell/AccountViewCell.xaml | 162 + .../AccountViewCell/AccountViewCell.xaml.cs | 55 + .../AccountViewCellViewModel.cs | 94 + .../AuthenticatorViewCell.xaml | 127 + .../AuthenticatorViewCell.xaml.cs | 68 + .../Bitwarden/Controls/AvatarImageSource.cs | 181 + .../Controls/AvatarImageSourcePool.cs | 33 + .../CipherViewCell/CipherViewCell.xaml | 127 + .../CipherViewCell/CipherViewCell.xaml.cs | 83 + .../CipherViewCell/CipherViewCellViewModel.cs | 51 + .../Controls/CircularProgressbarView.cs | 141 + src/Maui/Bitwarden/Controls/CustomLabel.cs | 14 + .../Controls/DateTime/DateTimePicker.xaml | 20 + .../Controls/DateTime/DateTimePicker.xaml.cs | 40 + .../Controls/DateTime/DateTimeViewModel.cs | 70 + .../Controls/ExtendedCollectionView.cs | 21 + .../Bitwarden/Controls/ExtendedDatePicker.cs | 87 + src/Maui/Bitwarden/Controls/ExtendedGrid.cs | 9 + .../Bitwarden/Controls/ExtendedSearchBar.cs | 22 + src/Maui/Bitwarden/Controls/ExtendedSlider.cs | 17 + .../Bitwarden/Controls/ExtendedStackLayout.cs | 9 + .../Bitwarden/Controls/ExtendedStepper.cs | 27 + .../Bitwarden/Controls/ExtendedTimePicker.cs | 87 + .../Bitwarden/Controls/ExtendedToolbarItem.cs | 30 + src/Maui/Bitwarden/Controls/HybridWebView.cs | 35 + src/Maui/Bitwarden/Controls/IconButton.cs | 26 + src/Maui/Bitwarden/Controls/IconLabel.cs | 27 + .../IconLabelButton/IconLabelButton.xaml | 42 + .../IconLabelButton/IconLabelButton.xaml.cs | 77 + src/Maui/Bitwarden/Controls/MiButton.cs | 23 + src/Maui/Bitwarden/Controls/MiLabel.cs | 22 + src/Maui/Bitwarden/Controls/MonoEntry.cs | 22 + src/Maui/Bitwarden/Controls/MonoLabel.cs | 22 + .../IPasswordStrengthable.cs | 11 + .../PasswordStrengthCategory.cs | 17 + .../PasswordStrengthProgressBar.xaml | 28 + .../PasswordStrengthProgressBar.xaml.cs | 110 + .../PasswordStrengthViewModel.cs | 68 + src/Maui/Bitwarden/Controls/RepeaterView.cs | 98 + .../Bitwarden/Controls/SelectableLabel.cs | 11 + .../Controls/SendViewCell/SendViewCell.xaml | 142 + .../SendViewCell/SendViewCell.xaml.cs | 69 + .../SendViewCell/SendViewCellViewModel.cs | 29 + .../Core/Abstractions/IApiService.cs | 99 + .../Core/Abstractions/IAppIdService.cs | 10 + .../Core/Abstractions/IAuditService.cs | 12 + .../Core/Abstractions/IAuthService.cs | 42 + .../Core/Abstractions/IAutofillHandler.cs | 16 + .../Abstractions/IAzureFileUpoadService.cs | 11 + .../Core/Abstractions/IBiometricService.cs | 10 + .../Core/Abstractions/IBroadcasterService.cs | 13 + .../Core/Abstractions/ICipherService.cs | 48 + .../Core/Abstractions/IClipboardService.cs | 19 + .../Core/Abstractions/ICollectionService.cs | 26 + .../IConditionedAwaiterManager.cs | 17 + .../Core/Abstractions/IConfigService.cs | 15 + .../Abstractions/ICryptoFunctionService.cs | 38 + .../Abstractions/ICryptoPrimitiveService.cs | 10 + .../Core/Abstractions/ICryptoService.cs | 57 + .../Core/Abstractions/IEnvironmentService.cs | 21 + .../Core/Abstractions/IEventService.cs | 12 + .../Core/Abstractions/IExportService.cs | 11 + .../Core/Abstractions/IFileService.cs | 14 + .../Core/Abstractions/IFileUploadService.cs | 12 + .../Core/Abstractions/IFolderService.cs | 26 + .../Core/Abstractions/II18nService.cs | 16 + .../Core/Abstractions/IKeyConnectorService.cs | 16 + .../Bitwarden/Core/Abstractions/ILogger.cs | 44 + .../Core/Abstractions/IMessagingService.cs | 7 + .../Core/Abstractions/INativeLogService.cs | 10 + .../Core/Abstractions/IOrganizationService.cs | 18 + .../IPasswordGenerationService.cs | 23 + .../Abstractions/IPlatformUtilsService.cs | 35 + .../Core/Abstractions/IPolicyService.cs | 25 + .../Core/Abstractions/ISearchService.cs | 23 + .../Core/Abstractions/ISendService.cs | 25 + .../Core/Abstractions/ISettingsService.cs | 13 + .../Abstractions/IStateMigrationService.cs | 9 + .../Core/Abstractions/IStateService.cs | 180 + .../Abstractions/IStorageMediatorService.cs | 16 + .../Core/Abstractions/IStorageService.cs | 11 + .../Core/Abstractions/ISyncService.cs | 20 + .../ISynchronousStorageService.cs | 9 + .../Core/Abstractions/ITokenService.cs | 33 + .../Core/Abstractions/ITotpService.cs | 10 + .../Abstractions/IUserVerificationService.cs | 10 + .../IUsernameGenerationService.cs | 13 + .../Core/Abstractions/IVaultTimeoutService.cs | 27 + .../Core/Abstractions/IWatchDeviceService.cs | 12 + .../Attributes/LocalizableEnumAttribute.cs | 14 + src/Maui/Bitwarden/Core/BitwardenIcons.cs | 119 + src/Maui/Bitwarden/Core/Constants.cs | 128 + .../Core/Enums/AuthenticationStatus.cs | 9 + .../Core/Enums/CipherRepromptType.cs | 8 + src/Maui/Bitwarden/Core/Enums/CipherType.cs | 13 + src/Maui/Bitwarden/Core/Enums/ClientType.cs | 36 + .../Core/Enums/CryptoHashAlgorithm.cs | 10 + src/Maui/Bitwarden/Core/Enums/DeviceType.cs | 27 + .../Bitwarden/Core/Enums/EncryptionType.cs | 13 + src/Maui/Bitwarden/Core/Enums/EventType.cs | 51 + src/Maui/Bitwarden/Core/Enums/FieldType.cs | 10 + .../Bitwarden/Core/Enums/FileUploadType.cs | 9 + .../Core/Enums/ForwardedEmailServiceType.cs | 19 + .../Bitwarden/Core/Enums/GeneratorType.cs | 12 + src/Maui/Bitwarden/Core/Enums/HashPurpose.cs | 8 + .../Bitwarden/Core/Enums/HdkfAlgorithm.cs | 8 + src/Maui/Bitwarden/Core/Enums/KdfType.cs | 8 + src/Maui/Bitwarden/Core/Enums/LinkedIdType.cs | 39 + .../Bitwarden/Core/Enums/NavigationTarget.cs | 14 + .../Bitwarden/Core/Enums/NotificationType.cs | 27 + .../Core/Enums/OrganizationUserStatusType.cs | 9 + .../Core/Enums/OrganizationUserType.cs | 11 + .../Bitwarden/Core/Enums/PaymentMethodType.cs | 12 + src/Maui/Bitwarden/Core/Enums/PlanType.cs | 13 + src/Maui/Bitwarden/Core/Enums/PolicyType.cs | 17 + .../Bitwarden/Core/Enums/SecureNoteType.cs | 7 + src/Maui/Bitwarden/Core/Enums/SendType.cs | 8 + .../Bitwarden/Core/Enums/StorageLocation.cs | 9 + .../Bitwarden/Core/Enums/TransactionType.cs | 11 + .../Core/Enums/TwoFactorProviderType.cs | 14 + src/Maui/Bitwarden/Core/Enums/UriMatchType.cs | 12 + .../Bitwarden/Core/Enums/UsernameEmailType.cs | 12 + src/Maui/Bitwarden/Core/Enums/UsernameType.cs | 16 + .../Core/Enums/VaultTimeoutAction.cs | 8 + .../Bitwarden/Core/Enums/VerificationType.cs | 8 + src/Maui/Bitwarden/Core/Enums/WatchState.cs | 13 + .../Bitwarden/Core/Exceptions/ApiException.cs | 22 + .../ForwardedEmailInvalidSecretException.cs | 11 + src/Maui/Bitwarden/Core/Models/Api/CardApi.cs | 12 + .../Bitwarden/Core/Models/Api/Fido2KeyApi.cs | 37 + .../Bitwarden/Core/Models/Api/FieldApi.cs | 12 + .../Bitwarden/Core/Models/Api/IdentityApi.cs | 24 + .../Bitwarden/Core/Models/Api/LoginApi.cs | 15 + .../Bitwarden/Core/Models/Api/LoginUriApi.cs | 10 + .../Core/Models/Api/SecureNoteApi.cs | 9 + .../Bitwarden/Core/Models/Api/SendFileApi.cs | 11 + .../Bitwarden/Core/Models/Api/SendTextApi.cs | 8 + .../Core/Models/Data/AttachmentData.cs | 26 + .../Bitwarden/Core/Models/Data/CardData.cs | 26 + .../Bitwarden/Core/Models/Data/CipherData.cs | 96 + .../Core/Models/Data/CollectionData.cs | 24 + src/Maui/Bitwarden/Core/Models/Data/Data.cs | 10 + .../Core/Models/Data/EnvironmentUrlData.cs | 24 + .../Bitwarden/Core/Models/Data/EventData.cs | 12 + .../Core/Models/Data/Fido2KeyData.cs | 34 + .../Bitwarden/Core/Models/Data/FieldData.cs | 23 + .../Bitwarden/Core/Models/Data/FolderData.cs | 23 + .../Core/Models/Data/IdentityData.cs | 50 + .../Bitwarden/Core/Models/Data/LoginData.cs | 29 + .../Core/Models/Data/LoginUriData.cs | 19 + .../Core/Models/Data/OrganizationData.cs | 58 + .../Core/Models/Data/PasswordHistoryData.cs | 19 + .../Bitwarden/Core/Models/Data/Permissions.cs | 19 + .../Bitwarden/Core/Models/Data/PolicyData.cs | 26 + .../Core/Models/Data/PreviousPageInfo.cs | 10 + .../Core/Models/Data/SecureNoteData.cs | 17 + .../Bitwarden/Core/Models/Data/SendData.cs | 60 + .../Core/Models/Data/SendFileData.cs | 25 + .../Core/Models/Data/SendTextData.cs | 19 + .../Bitwarden/Core/Models/Domain/Account.cs | 125 + .../Core/Models/Domain/Attachment.cs | 68 + .../Core/Models/Domain/AuthResult.cs | 15 + src/Maui/Bitwarden/Core/Models/Domain/Card.cs | 46 + .../Bitwarden/Core/Models/Domain/Cipher.cs | 216 + .../Core/Models/Domain/Collection.cs | 41 + .../Bitwarden/Core/Models/Domain/Domain.cs | 82 + .../Core/Models/Domain/EncByteArray.cs | 12 + .../Bitwarden/Core/Models/Domain/EncString.cs | 125 + .../Core/Models/Domain/EnvironmentUrls.cs | 10 + .../Bitwarden/Core/Models/Domain/Fido2Key.cs | 54 + .../Bitwarden/Core/Models/Domain/Field.cs | 53 + .../Bitwarden/Core/Models/Domain/Folder.cs | 32 + .../Models/Domain/ForcePasswordResetReason.cs | 16 + .../Models/Domain/GeneratedPasswordHistory.cs | 10 + .../Core/Models/Domain/ITreeNodeObject.cs | 8 + .../Bitwarden/Core/Models/Domain/Identity.cs | 70 + .../Core/Models/Domain/KdfConfiguration.cs | 27 + .../Bitwarden/Core/Models/Domain/Login.cs | 78 + .../Bitwarden/Core/Models/Domain/LoginUri.cs | 39 + .../Domain/MasterPasswordPolicyOptions.cs | 23 + .../Bitwarden/Core/Models/Domain/Message.cs | 8 + .../Core/Models/Domain/Organization.cs | 103 + .../Domain/PasswordGenerationOptions.cs | 140 + .../Domain/PasswordGeneratorPolicyOptions.cs | 32 + .../Core/Models/Domain/PasswordHistory.cs | 40 + .../Bitwarden/Core/Models/Domain/Policy.cs | 58 + .../Domain/ResetPasswordPolicyOptions.cs | 7 + .../Core/Models/Domain/SecureNote.cs | 32 + src/Maui/Bitwarden/Core/Models/Domain/Send.cs | 93 + .../Bitwarden/Core/Models/Domain/SendFile.cs | 26 + .../Bitwarden/Core/Models/Domain/SendText.cs | 25 + .../Bitwarden/Core/Models/Domain/State.cs | 10 + .../Core/Models/Domain/StorageOptions.cs | 13 + .../Core/Models/Domain/SymmetricCryptoKey.cs | 77 + .../Bitwarden/Core/Models/Domain/TreeNode.cs | 18 + .../Core/Models/Domain/TwoFactorProvider.cs | 14 + .../Domain/UsernameGenerationOptions.cs | 57 + src/Maui/Bitwarden/Core/Models/Export/Card.cs | 52 + .../Bitwarden/Core/Models/Export/Cipher.cs | 125 + .../Core/Models/Export/CipherWithId.cs | 26 + .../Core/Models/Export/Collection.cs | 44 + .../Core/Models/Export/CollectionWithId.cs | 21 + .../Bitwarden/Core/Models/Export/Field.cs | 41 + .../Bitwarden/Core/Models/Export/Folder.cs | 32 + .../Core/Models/Export/FolderWithId.cs | 21 + .../Bitwarden/Core/Models/Export/Identity.cs | 100 + .../Bitwarden/Core/Models/Export/Login.cs | 49 + .../Bitwarden/Core/Models/Export/LoginUri.cs | 37 + .../Core/Models/Export/SecureNote.cs | 33 + .../Core/Models/Request/AttachmentRequest.cs | 11 + .../Request/CipherCollectionsRequest.cs | 14 + .../Models/Request/CipherCreateRequest.cs | 18 + .../Core/Models/Request/CipherRequest.cs | 133 + .../Core/Models/Request/CipherShareRequest.cs | 18 + .../Models/Request/DeleteAccountRequest.cs | 9 + .../Core/Models/Request/DeviceRequest.cs | 20 + .../Core/Models/Request/DeviceTokenRequest.cs | 7 + .../Core/Models/Request/EventRequest.cs | 12 + .../Core/Models/Request/FolderRequest.cs | 14 + .../Request/KeyConnectorUserKeyRequest.cs | 13 + .../Core/Models/Request/KeysRequest.cs | 8 + .../OrganizationSsoDomainDetailsRequest.cs | 9 + ...ationUserResetPasswordEnrollmentRequest.cs | 8 + .../Models/Request/PasswordHintRequest.cs | 7 + .../Models/Request/PasswordHistoryRequest.cs | 10 + .../Core/Models/Request/PasswordRequest.cs | 10 + .../Request/PasswordVerificationRequest.cs | 7 + .../Request/PasswordlessCreateLoginRequest.cs | 34 + .../Request/PasswordlessLoginRequest.cs | 21 + .../Core/Models/Request/PreloginRequest.cs | 7 + .../Core/Models/Request/RegisterRequest.cs | 22 + .../Core/Models/Request/SendRequest.cs | 58 + .../Request/SetKeyConnectorKeyRequest.cs | 27 + .../Core/Models/Request/SetPasswordRequest.cs | 17 + .../Core/Models/Request/TokenRequest.cs | 106 + .../Models/Request/TwoFactorEmailRequest.cs | 9 + .../Request/UpdateTempPasswordRequest.cs | 9 + .../Core/Models/Request/VerifyOTPRequest.cs | 13 + .../Models/Response/AttachmentResponse.cs | 12 + .../Response/AttachmentUploadDataReponse.cs | 12 + .../Models/Response/BreachAccountResponse.cs | 20 + .../Core/Models/Response/CipherResponse.cs | 34 + .../Models/Response/CollectionResponse.cs | 15 + .../Core/Models/Response/ConfigResponse.cs | 31 + .../Core/Models/Response/DomainsResponse.cs | 10 + .../Core/Models/Response/ErrorResponse.cs | 103 + .../Core/Models/Response/FolderResponse.cs | 11 + .../Models/Response/GlobalDomainResponse.cs | 11 + .../Response/IdentityCaptchaResponse.cs | 13 + .../Core/Models/Response/IdentityResponse.cs | 50 + .../Models/Response/IdentityTokenResponse.cs | 33 + .../Response/IdentityTwoFactorResponse.cs | 16 + .../Response/KeyConnectorUserKeyResponse.cs | 9 + .../Models/Response/NotificationResponse.cs | 42 + .../OrganizationAutoEnrollStatusResponse.cs | 8 + .../OrganizationDomainSsoDetailsResponse.cs | 13 + .../Response/OrganizationKeysResponse.cs | 8 + .../Response/PasswordHistoryResponse.cs | 10 + .../Response/PasswordlessLoginResponse.cs | 33 + .../Core/Models/Response/PolicyResponse.cs | 14 + .../Core/Models/Response/PreloginResponse.cs | 15 + .../Response/ProfileOrganizationResponse.cs | 32 + .../Core/Models/Response/ProfileResponse.cs | 24 + .../Response/SendFileUploadDataResponse.cs | 12 + .../Core/Models/Response/SendResponse.cs | 26 + .../Models/Response/SsoPrevalidateResponse.cs | 7 + .../Core/Models/Response/SyncResponse.cs | 15 + .../Response/VerifyMasterPasswordResponse.cs | 9 + .../Bitwarden/Core/Models/View/AccountView.cs | 43 + .../Core/Models/View/AttachmentView.cs | 36 + .../Bitwarden/Core/Models/View/CardView.cs | 105 + .../Bitwarden/Core/Models/View/CipherView.cs | 126 + .../Core/Models/View/CollectionView.cs | 23 + .../Core/Models/View/Fido2KeyView.cs | 26 + .../Bitwarden/Core/Models/View/FieldView.cs | 24 + .../Bitwarden/Core/Models/View/FolderView.cs | 20 + .../Core/Models/View/ILaunchableView.cs | 8 + .../Core/Models/View/IdentityView.cs | 173 + .../Bitwarden/Core/Models/View/ItemView.cs | 14 + .../Core/Models/View/LoginUriView.cs | 113 + .../Bitwarden/Core/Models/View/LoginView.cs | 41 + .../Core/Models/View/PasswordHistoryView.cs | 18 + .../Core/Models/View/SecureNoteView.cs | 20 + .../Core/Models/View/SendFileView.cs | 23 + .../Core/Models/View/SendTextView.cs | 17 + .../Bitwarden/Core/Models/View/SendView.cs | 51 + .../Core/Models/View/SimpleCipherView.cs | 68 + src/Maui/Bitwarden/Core/Models/View/View.cs | 5 + .../Bitwarden/Core/Models/View/WatchDTO.cs | 64 + .../Core/Resources/eff_long_word_list.txt | 7776 ++++++++++ .../Core/Resources/public_suffix_list.dat | 12653 ++++++++++++++++ .../Bitwarden/Core/Services/ApiService.cs | 886 ++ .../Bitwarden/Core/Services/AppIdService.cs | 38 + .../Bitwarden/Core/Services/AuditService.cs | 62 + .../Bitwarden/Core/Services/AuthService.cs | 642 + .../Core/Services/AzureFileUploadService.cs | 197 + .../Services/BitwardenFileUploadService.cs | 29 + .../Core/Services/BroadcasterService.cs | 86 + .../Bitwarden/Core/Services/CipherService.cs | 1345 ++ .../Core/Services/CollectionService.cs | 229 + .../Services/ConditionedAwaiterManager.cs | 42 + .../Bitwarden/Core/Services/ConfigService.cs | 77 + .../Core/Services/ConsoleLogService.cs | 28 + .../Bitwarden/Core/Services/CryptoService.cs | 913 ++ .../EmailForwarders/AnonAddyForwarder.cs | 42 + .../Services/EmailForwarders/BaseForwarder.cs | 63 + .../EmailForwarders/DuckDuckGoForwarder.cs | 25 + .../EmailForwarders/FastmailForwarder.cs | 98 + .../EmailForwarders/FirefoxRelayForwarder.cs | 36 + .../EmailForwarders/ForwarderOptions.cs | 7 + .../EmailForwarders/SimpleLoginForwarder.cs | 25 + .../Core/Services/EnvironmentService.cs | 147 + .../Bitwarden/Core/Services/EventService.cs | 106 + .../Bitwarden/Core/Services/ExportService.cs | 227 + .../Core/Services/FileUploadService.cs | 83 + .../Bitwarden/Core/Services/FolderService.cs | 268 + .../Core/Services/InMemoryStorageService.cs | 40 + .../Core/Services/KeyConnectorService.cs | 87 + .../Core/Services/LiteDbStorageService.cs | 123 + .../Core/Services/Logging/DebugLogger.cs | 57 + .../Bitwarden/Core/Services/Logging/Logger.cs | 136 + .../Core/Services/Logging/LoggerHelper.cs | 31 + .../Core/Services/Logging/StubLogger.cs | 28 + .../Core/Services/OrganizationService.cs | 81 + .../Services/PasswordGenerationService.cs | 547 + .../Core/Services/PclCryptoFunctionService.cs | 312 + .../Bitwarden/Core/Services/PolicyService.cs | 362 + .../Bitwarden/Core/Services/SearchService.cs | 170 + .../Bitwarden/Core/Services/SendService.cs | 300 + .../Core/Services/SettingsService.cs | 80 + .../Core/Services/StateMigrationService.cs | 616 + .../Bitwarden/Core/Services/StateService.cs | 1660 ++ .../Core/Services/StorageMediatorService.cs | 62 + .../Bitwarden/Core/Services/SyncService.cs | 435 + .../Bitwarden/Core/Services/TokenService.cs | 236 + .../Bitwarden/Core/Services/TotpService.cs | 116 + .../Core/Services/UserVerificationService.cs | 67 + .../Services/UsernameGenerationService.cs | 185 + .../Core/Services/VaultTimeoutService.cs | 250 + .../AccountsManagerMessageCommands.cs | 14 + src/Maui/Bitwarden/Core/Utilities/Base32.cs | 72 + .../Core/Utilities/CipherTypeExtensions.cs | 14 + .../Bitwarden/Core/Utilities/CoreHelpers.cs | 298 + .../Bitwarden/Core/Utilities/DomainName.cs | 340 + .../Core/Utilities/EEFLongWordList.cs | 52 + .../Utilities/ExtendedObservableCollection.cs | 46 + .../Core/Utilities/ExtendedViewModel.cs | 38 + .../Bitwarden/Core/Utilities/LazyResolve.cs | 17 + src/Maui/Bitwarden/Core/Utilities/OtpData.cs | 94 + .../Core/Utilities/ServiceContainer.cs | 233 + .../Core/Utilities/StringExtensions.cs | 34 + .../Core/Utilities/TaskExtensions.cs | 28 + .../Bitwarden/Core/Utilities/UriExtensions.cs | 18 + src/Maui/Bitwarden/Effects/FabShadowEffect.cs | 12 + src/Maui/Bitwarden/Effects/FixedSizeEffect.cs | 12 + .../Effects/NoEmojiKeyboardEffect.cs | 13 + .../Effects/RemoveFontPaddingEffect.cs | 14 + .../Bitwarden/Effects/ScrollEnabledEffect.cs | 26 + ...iewContentInsetAdjustmentBehaviorEffect.cs | 34 + src/Maui/Bitwarden/Effects/TabBarEffect.cs | 12 + .../CustomFieldItemTemplateSelector.cs | 29 + .../BooleanCustomFieldItemLayout.xaml | 74 + .../BooleanCustomFieldItemLayout.xaml.cs | 13 + .../HiddenCustomFieldItemLayout.xaml | 104 + .../HiddenCustomFieldItemLayout.xaml.cs | 13 + .../LinkedCustomFieldItemLayout.xaml | 65 + .../LinkedCustomFieldItemLayout.xaml.cs | 13 + .../TextCustomFieldItemLayout.xaml | 74 + .../TextCustomFieldItemLayout.xaml.cs | 13 + .../BaseCustomFieldItemViewModel.cs | 50 + .../BooleanCustomFieldItemViewModel.cs | 23 + .../CustomFields/CustomFieldItemFactory.cs | 53 + .../HiddenCustomFieldItemViewModel.cs | 70 + .../CustomFields/ICustomFieldItemViewModel.cs | 13 + .../LinkedCustomFieldItemViewModel.cs | 70 + .../TextCustomFieldItemViewModel.cs | 20 + src/Maui/Bitwarden/MauiProgram.cs | 40 + src/Maui/Bitwarden/Models/AppOptions.cs | 56 + src/Maui/Bitwarden/Models/DialogDetails.cs | 12 + src/Maui/Bitwarden/Models/NotificationData.cs | 23 + src/Maui/Bitwarden/Models/PlatformCulture.cs | 39 + .../Accounts/BaseChangePasswordViewModel.cs | 176 + .../Pages/Accounts/DeleteAccountPage.xaml | 81 + .../Pages/Accounts/DeleteAccountPage.xaml.cs | 34 + .../Pages/Accounts/DeleteAccountViewModel.cs | 110 + .../Pages/Accounts/EnvironmentPage.xaml | 102 + .../Pages/Accounts/EnvironmentPage.xaml.cs | 55 + .../Accounts/EnvironmentPageViewModel.cs | 91 + .../Bitwarden/Pages/Accounts/HintPage.xaml | 40 + .../Bitwarden/Pages/Accounts/HintPage.xaml.cs | 37 + .../Pages/Accounts/HintPageViewModel.cs | 86 + .../Bitwarden/Pages/Accounts/HomePage.xaml | 145 + .../Bitwarden/Pages/Accounts/HomePage.xaml.cs | 149 + .../Pages/Accounts/HomePageViewModel.cs | 220 + .../Bitwarden/Pages/Accounts/LockPage.xaml | 183 + .../Bitwarden/Pages/Accounts/LockPage.xaml.cs | 206 + .../Pages/Accounts/LockPageViewModel.cs | 470 + .../Bitwarden/Pages/Accounts/LoginPage.xaml | 175 + .../Pages/Accounts/LoginPage.xaml.cs | 211 + .../Pages/Accounts/LoginPageViewModel.cs | 351 + .../Pages/Accounts/LoginPasswordlessPage.xaml | 92 + .../Accounts/LoginPasswordlessPage.xaml.cs | 41 + .../LoginPasswordlessRequestPage.xaml | 80 + .../LoginPasswordlessRequestPage.xaml.cs | 66 + .../LoginPasswordlessRequestViewModel.cs | 194 + .../Accounts/LoginPasswordlessViewModel.cs | 176 + .../Pages/Accounts/LoginSsoPage.xaml | 46 + .../Pages/Accounts/LoginSsoPage.xaml.cs | 125 + .../Pages/Accounts/LoginSsoPageViewModel.cs | 260 + .../Pages/Accounts/RegisterPage.xaml | 194 + .../Pages/Accounts/RegisterPage.xaml.cs | 77 + .../Pages/Accounts/RegisterPageViewModel.cs | 291 + .../Accounts/RemoveMasterPasswordPage.xaml | 33 + .../Accounts/RemoveMasterPasswordPage.xaml.cs | 59 + .../RemoveMasterPasswordPageViewModel.cs | 56 + .../Pages/Accounts/SetPasswordPage.xaml | 170 + .../Pages/Accounts/SetPasswordPage.xaml.cs | 79 + .../Accounts/SetPasswordPageViewModel.cs | 311 + .../Pages/Accounts/TwoFactorPage.xaml | 179 + .../Pages/Accounts/TwoFactorPage.xaml.cs | 208 + .../Pages/Accounts/TwoFactorPageViewModel.cs | 428 + .../Accounts/UpdateTempPasswordPage.xaml | 199 + .../Accounts/UpdateTempPasswordPage.xaml.cs | 87 + .../UpdateTempPasswordPageViewModel.cs | 172 + .../Pages/Accounts/VerificationCodePage.xaml | 99 + .../Accounts/VerificationCodePage.xaml.cs | 49 + .../Accounts/VerificationCodeViewModel.cs | 177 + src/Maui/Bitwarden/Pages/BaseContentPage.cs | 159 + src/Maui/Bitwarden/Pages/BaseViewModel.cs | 52 + .../Pages/CaptchaProtectedViewModel.cs | 85 + .../Bitwarden/Pages/CollectionViewModel.cs | 16 + .../Pages/Generator/GeneratorHistoryPage.xaml | 102 + .../Generator/GeneratorHistoryPage.xaml.cs | 76 + .../GeneratorHistoryPageViewModel.cs | 79 + .../Pages/Generator/GeneratorPage.xaml | 549 + .../Pages/Generator/GeneratorPage.xaml.cs | 162 + .../Pages/Generator/GeneratorPageViewModel.cs | 864 ++ .../Bitwarden/Pages/Send/SendAddEditPage.xaml | 553 + .../Pages/Send/SendAddEditPage.xaml.cs | 352 + .../Pages/Send/SendAddEditPageViewModel.cs | 654 + .../Pages/Send/SendAddOnlyOptionsView.xaml | 183 + .../Pages/Send/SendAddOnlyOptionsView.xaml.cs | 97 + .../Bitwarden/Pages/Send/SendAddOnlyPage.xaml | 191 + .../Pages/Send/SendAddOnlyPage.xaml.cs | 179 + .../ISendGroupingsPageListItem.cs | 6 + .../SendGroupingsPage/SendGroupingsPage.xaml | 176 + .../SendGroupingsPage.xaml.cs | 207 + .../SendGroupingsPageHeaderListItem.cs | 14 + .../SendGroupingsPageListGroup.cs | 35 + .../SendGroupingsPageListItem.cs | 92 + .../SendGroupingsPageListItemSelector.cs | 26 + .../SendGroupingsPageViewModel.cs | 343 + src/Maui/Bitwarden/Pages/Send/SendsPage.xaml | 85 + .../Bitwarden/Pages/Send/SendsPage.xaml.cs | 122 + .../Pages/Send/SendsPageViewModel.cs | 129 + .../Pages/Settings/AutofillPage.xaml | 48 + .../Pages/Settings/AutofillPage.xaml.cs | 20 + .../Pages/Settings/AutofillServicesPage.xaml | 131 + .../Settings/AutofillServicesPage.xaml.cs | 68 + .../Settings/AutofillServicesPageViewModel.cs | 231 + .../Pages/Settings/BlockAutofillUrisPage.xaml | 85 + .../Settings/BlockAutofillUrisPage.xaml.cs | 44 + .../BlockAutofillUrisPageViewModel.cs | 186 + .../Pages/Settings/ExportVaultPage.xaml | 136 + .../Pages/Settings/ExportVaultPage.xaml.cs | 84 + .../Settings/ExportVaultPageViewModel.cs | 263 + .../Pages/Settings/ExtensionPage.xaml | 95 + .../Pages/Settings/ExtensionPage.xaml.cs | 38 + .../Pages/Settings/ExtensionPageViewModel.cs | 66 + .../Pages/Settings/FolderAddEditPage.xaml | 58 + .../Pages/Settings/FolderAddEditPage.xaml.cs | 90 + .../Settings/FolderAddEditPageViewModel.cs | 145 + .../Bitwarden/Pages/Settings/FoldersPage.xaml | 86 + .../Pages/Settings/FoldersPage.xaml.cs | 71 + .../Pages/Settings/FoldersPageViewModel.cs | 45 + .../LoginPasswordlessRequestsListPage.xaml | 107 + .../LoginPasswordlessRequestsListPage.xaml.cs | 39 + .../LoginPasswordlessRequestsListViewModel.cs | 140 + .../Bitwarden/Pages/Settings/OptionsPage.xaml | 169 + .../Pages/Settings/OptionsPage.xaml.cs | 55 + .../Pages/Settings/OptionsPageViewModel.cs | 302 + .../SettingsPage/ISettingsPageListItem.cs | 6 + .../Settings/SettingsPage/SettingsPage.xaml | 123 + .../SettingsPage/SettingsPage.xaml.cs | 75 + .../SettingsPageHeaderListItem.cs | 12 + .../SettingsPage/SettingsPageListGroup.cs | 29 + .../SettingsPage/SettingsPageListItem.cs | 53 + .../SettingsPageListItemSelector.cs | 25 + .../SettingsPage/SettingsPageViewModel.cs | 882 ++ .../Bitwarden/Pages/Settings/SyncPage.xaml | 51 + .../Bitwarden/Pages/Settings/SyncPage.xaml.cs | 45 + .../Pages/Settings/SyncPageViewModel.cs | 117 + src/Maui/Bitwarden/Pages/TabsPage.cs | 180 + .../Pages/Vault/AttachmentsPage.xaml | 105 + .../Pages/Vault/AttachmentsPage.xaml.cs | 73 + .../Pages/Vault/AttachmentsPageViewModel.cs | 210 + .../Vault/AutofillCiphersPageViewModel.cs | 152 + .../Pages/Vault/BaseCipherViewModel.cs | 79 + .../Pages/Vault/CipherAddEditPage.xaml | 879 ++ .../Pages/Vault/CipherAddEditPage.xaml.cs | 402 + .../Pages/Vault/CipherAddEditPageViewModel.cs | 917 ++ .../Pages/Vault/CipherDetailsPage.xaml | 823 + .../Pages/Vault/CipherDetailsPage.xaml.cs | 328 + .../Pages/Vault/CipherDetailsPageViewModel.cs | 728 + .../Pages/Vault/CipherSelectionPage.xaml | 160 + .../Pages/Vault/CipherSelectionPage.xaml.cs | 188 + .../Vault/CipherSelectionPageViewModel.cs | 169 + .../Bitwarden/Pages/Vault/CiphersPage.xaml | 123 + .../Bitwarden/Pages/Vault/CiphersPage.xaml.cs | 139 + .../Pages/Vault/CiphersPageViewModel.cs | 276 + .../Pages/Vault/CollectionsPage.xaml | 58 + .../Pages/Vault/CollectionsPage.xaml.cs | 51 + .../Pages/Vault/CollectionsPageViewModel.cs | 97 + .../Vault/GroupingsPage/GroupingsPage.xaml | 215 + .../Vault/GroupingsPage/GroupingsPage.xaml.cs | 339 + .../GroupingsPageHeaderListItem.cs | 23 + .../GroupingsPage/GroupingsPageListGroup.cs | 38 + .../GroupingsPage/GroupingsPageListItem.cs | 159 + .../GroupingsPageListItemSelector.cs | 33 + .../GroupingsPageTOTPListItem.cs | 121 + .../GroupingsPage/GroupingsPageViewModel.cs | 710 + .../GroupingsPage/IGroupingsPageListItem.cs | 6 + .../Vault/OTPCipherSelectionPageViewModel.cs | 80 + .../Pages/Vault/PasswordHistoryPage.xaml | 87 + .../Pages/Vault/PasswordHistoryPage.xaml.cs | 42 + .../Vault/PasswordHistoryPageViewModel.cs | 58 + src/Maui/Bitwarden/Pages/Vault/ScanPage.xaml | 144 + .../Bitwarden/Pages/Vault/ScanPage.xaml.cs | 318 + .../Pages/Vault/ScanPageViewModel.cs | 129 + src/Maui/Bitwarden/Pages/Vault/SharePage.xaml | 85 + .../Bitwarden/Pages/Vault/SharePage.xaml.cs | 45 + .../Pages/Vault/SharePageViewModel.cs | 175 + .../Bitwarden/Pages/VaultFilterViewModel.cs | 123 + .../Platforms/Android/8bit.keystore.enc | Bin 0 -> 2288 bytes .../Accessibility/AccessibilityActivity.cs | 123 + .../Accessibility/AccessibilityHelpers.cs | 919 ++ .../Accessibility/AccessibilityService.cs | 472 + .../Android/Accessibility/Browser.cs | 23 + .../Android/Accessibility/Credentials.cs | 10 + .../Accessibility/KnownUsernameField.cs | 14 + .../Android/Accessibility/NodeList.cs | 18 + .../Platforms/Android/AndroidManifest.xml | 55 + .../Android/Autofill/AutofillConstants.cs | 10 + .../AutofillExternalSelectionActivity.cs | 42 + .../Android/Autofill/AutofillHelpers.cs | 438 + .../Android/Autofill/AutofillService.cs | 163 + .../Platforms/Android/Autofill/Field.cs | 195 + .../Android/Autofill/FieldCollection.cs | 359 + .../Platforms/Android/Autofill/FilledItem.cs | 226 + .../Platforms/Android/Autofill/Parser.cs | 176 + .../Platforms/Android/Autofill/SavedItem.cs | 26 + .../Bitwarden/Platforms/Android/Constants.cs | 7 + .../Android/Effects/FabShadowEffect.cs | 29 + .../Android/Effects/FixedSizeEffect.cs | 22 + .../Android/Effects/NoEmojiKeyboardEffect.cs | 23 + .../Effects/RemoveFontPaddingEffect.cs | 22 + .../Platforms/Android/Effects/TabBarEffect.cs | 32 + .../Platforms/Android/MainActivity.cs | 464 + .../Platforms/Android/MainApplication.cs | 230 + .../Android/Push/FirebaseMessagingService.cs | 60 + .../Receivers/ClearClipboardAlarmReceiver.cs | 25 + .../Android/Receivers/EventUploadReceiver.cs | 16 + .../Receivers/NotificationDismissReceiver.cs | 41 + .../Receivers/PackageReplacedReceiver.cs | 22 + .../Receivers/RestrictionsChangedReceiver.cs | 23 + .../Resources/drawable-hdpi/logo_legacy.png | Bin 0 -> 6997 bytes .../drawable-hdpi/logo_white_legacy.png | Bin 0 -> 6945 bytes .../Resources/drawable-hdpi/yubikey.png | Bin 0 -> 110977 bytes .../splash_screen_round.xml | 5 + .../Resources/drawable-v23/splash_screen.xml | 15 + .../drawable-v23/splash_screen_dark.xml | 15 + .../drawable-v26/splash_screen_round.xml | 5 + .../Resources/drawable-xhdpi/logo_legacy.png | Bin 0 -> 10888 bytes .../drawable-xhdpi/logo_white_legacy.png | Bin 0 -> 10486 bytes .../Resources/drawable-xhdpi/yubikey.png | Bin 0 -> 191579 bytes .../Resources/drawable-xxhdpi/logo_legacy.png | Bin 0 -> 8426 bytes .../drawable-xxhdpi/logo_white_legacy.png | Bin 0 -> 8402 bytes .../Resources/drawable-xxhdpi/yubikey.png | Bin 0 -> 390403 bytes .../Android/Resources/drawable/card.xml | 9 + .../Resources/drawable/cog_environment.xml | 9 + .../Resources/drawable/cog_settings.xml | 9 + .../drawable/empty_uris_placeholder.xml | 35 + .../drawable/empty_uris_placeholder_dark.xml | 35 + .../Android/Resources/drawable/generate.xml | 9 + .../drawable/ic_launcher_foreground.xml | 33 + .../drawable/ic_launcher_monochrome.xml | 15 + .../Resources/drawable/ic_notification.xml | 4 + .../Android/Resources/drawable/ic_warning.xml | 9 + .../Android/Resources/drawable/icon.xml | 30 + .../Android/Resources/drawable/id.xml | 9 + .../Android/Resources/drawable/info.xml | 9 + .../Resources/drawable/list_item_bg.xml | 7 + .../Android/Resources/drawable/lock.xml | 9 + .../Android/Resources/drawable/login.xml | 9 + .../Android/Resources/drawable/logo.xml | 9 + .../Resources/drawable/logo_rounded.xml | 14 + .../Android/Resources/drawable/logo_white.xml | 9 + .../Android/Resources/drawable/pencil.xml | 9 + .../Android/Resources/drawable/plus.xml | 9 + .../Android/Resources/drawable/search.xml | 9 + .../Android/Resources/drawable/send.xml | 9 + .../Android/Resources/drawable/shield.xml | 9 + .../Resources/drawable/slider_thumb.xml | 6 + .../Resources/drawable/splash_screen.xml | 9 + .../Resources/drawable/splash_screen_dark.xml | 9 + .../Resources/drawable/switch_thumb.xml | 5 + .../Android/Resources/layout/Tabbar.axml | 11 + .../Android/Resources/layout/Toolbar.axml | 9 + .../Resources/layout/autofill_listitem.xml | 40 + .../layout/progress_dialog_layout.xml | 27 + .../validatable_input_dialog_layout.xml | 27 + .../mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + .../Resources/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1859 bytes .../mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 3514 bytes .../Resources/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1370 bytes .../mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2269 bytes .../Resources/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2502 bytes .../mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 5013 bytes .../Resources/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 3671 bytes .../mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 7731 bytes .../Resources/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 5013 bytes .../mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 11107 bytes .../Android/Resources/values-night/styles.xml | 27 + .../Android/Resources/values-v30/manifest.xml | 4 + .../Android/Resources/values/colors.xml | 22 + .../Android/Resources/values/dimens.xml | 8 + .../values/ic_launcher_background.xml | 4 + .../Android/Resources/values/manifest.xml | 4 + .../Android/Resources/values/strings.xml | 28 + .../Android/Resources/values/styles.xml | 29 + .../Resources/xml/accessibilityservice.xml | 10 + .../Resources/xml/app_restrictions.xml | 9 + .../Android/Resources/xml/autofillservice.xml | 281 + .../Android/Resources/xml/filepaths.xml | 5 + .../Resources/xml/network_security_config.xml | 17 + .../Android/Services/AndroidLogService.cs | 30 + .../AndroidPushNotificationService.cs | 110 + .../Android/Services/AutofillHandler.cs | 215 + .../Android/Services/BiometricService.cs | 124 + .../Android/Services/ClipboardService.cs | 84 + .../Services/CryptoPrimitiveService.cs | 53 + .../Android/Services/DeviceActionService.cs | 627 + .../Platforms/Android/Services/FileService.cs | 278 + .../Android/Services/LocalizeService.cs | 113 + .../Android/Services/WatchDeviceService.cs | 28 + .../Android/Tiles/AutofillTileService.cs | 94 + .../Android/Tiles/GeneratorTileService.cs | 68 + .../Android/Tiles/MyVaultTileService.cs | 69 + .../Android/Utilities/AndroidHelpers.cs | 70 + .../Android/Utilities/IntentExtensions.cs | 28 + .../Android/Utilities/ThemeHelpers.cs | 76 + .../Android/WebAuthCallbackActivity.cs | 24 + .../Platforms/Android/fdroid-keystore.jks.enc | Bin 0 -> 2144 bytes .../Platforms/Android/google-services.json | 42 + .../Android/google-services.json.enc | Bin 0 -> 1520 bytes .../Platforms/Android/upload-keystore.jks.enc | Bin 0 -> 2288 bytes .../Platforms/MacCatalyst/AppDelegate.cs | 10 + .../Platforms/MacCatalyst/Info.plist | 30 + .../Platforms/MacCatalyst/Program.cs | 16 + src/Maui/Bitwarden/Platforms/Tizen/Main.cs | 17 + .../Platforms/Tizen/tizen-manifest.xml | 15 + src/Maui/Bitwarden/Platforms/Windows/App.xaml | 9 + .../Bitwarden/Platforms/Windows/App.xaml.cs | 25 + .../Platforms/Windows/Package.appxmanifest | 47 + .../Bitwarden/Platforms/Windows/app.manifest | 16 + .../Bitwarden/Platforms/iOS/AppDelegate.cs | 10 + src/Maui/Bitwarden/Platforms/iOS/Info.plist | 32 + src/Maui/Bitwarden/Platforms/iOS/Program.cs | 16 + .../Bitwarden/Properties/launchSettings.json | 8 + .../Bitwarden/Resources/AppIcon/appicon.svg | 5 + .../Bitwarden/Resources/AppIcon/appiconfg.svg | 8 + .../Resources/AppResources.Designer.cs | 7392 +++++++++ .../Bitwarden/Resources/AppResources.af.resx | 2716 ++++ .../Bitwarden/Resources/AppResources.ar.resx | 2717 ++++ .../Bitwarden/Resources/AppResources.az.resx | 2715 ++++ .../Bitwarden/Resources/AppResources.be.resx | 2716 ++++ .../Bitwarden/Resources/AppResources.bg.resx | 2716 ++++ .../Bitwarden/Resources/AppResources.bn.resx | 2717 ++++ .../Bitwarden/Resources/AppResources.bs.resx | 2715 ++++ .../Bitwarden/Resources/AppResources.ca.resx | 2716 ++++ .../Resources/AppResources.cs.Designer.cs | 0 .../Bitwarden/Resources/AppResources.cs.resx | 2715 ++++ .../Bitwarden/Resources/AppResources.cy.resx | 2717 ++++ .../Resources/AppResources.da.Designer.cs | 0 .../Bitwarden/Resources/AppResources.da.resx | 2716 ++++ .../Resources/AppResources.de.Designer.cs | 0 .../Bitwarden/Resources/AppResources.de.resx | 2715 ++++ .../Bitwarden/Resources/AppResources.el.resx | 2716 ++++ .../Resources/AppResources.en-GB.resx | 2716 ++++ .../Resources/AppResources.en-IN.resx | 2730 ++++ .../Resources/AppResources.es.Designer.cs | 0 .../Bitwarden/Resources/AppResources.es.resx | 2717 ++++ .../Bitwarden/Resources/AppResources.et.resx | 2716 ++++ .../Bitwarden/Resources/AppResources.eu.resx | 2715 ++++ .../Bitwarden/Resources/AppResources.fa.resx | 2717 ++++ .../Resources/AppResources.fi.Designer.cs | 0 .../Bitwarden/Resources/AppResources.fi.resx | 2717 ++++ .../Bitwarden/Resources/AppResources.fil.resx | 2717 ++++ .../Resources/AppResources.fr.Designer.cs | 0 .../Bitwarden/Resources/AppResources.fr.resx | 2717 ++++ .../Bitwarden/Resources/AppResources.gl.resx | 2717 ++++ .../Bitwarden/Resources/AppResources.he.resx | 2719 ++++ .../Resources/AppResources.hi.Designer.cs | 0 .../Bitwarden/Resources/AppResources.hi.resx | 2717 ++++ .../Resources/AppResources.hr.Designer.cs | 0 .../Bitwarden/Resources/AppResources.hr.resx | 2714 ++++ .../Resources/AppResources.hu.Designer.cs | 0 .../Bitwarden/Resources/AppResources.hu.resx | 2715 ++++ .../Resources/AppResources.id.Designer.cs | 0 .../Bitwarden/Resources/AppResources.id.resx | 2716 ++++ .../Resources/AppResources.it.Designer.cs | 0 .../Bitwarden/Resources/AppResources.it.resx | 2716 ++++ .../Resources/AppResources.ja.Designer.cs | 0 .../Bitwarden/Resources/AppResources.ja.resx | 2716 ++++ .../Bitwarden/Resources/AppResources.ka.resx | 2717 ++++ .../Bitwarden/Resources/AppResources.kn.resx | 2717 ++++ .../Bitwarden/Resources/AppResources.ko.resx | 2716 ++++ .../Bitwarden/Resources/AppResources.lt.resx | 2717 ++++ .../Bitwarden/Resources/AppResources.lv.resx | 2716 ++++ .../Bitwarden/Resources/AppResources.ml.resx | 2716 ++++ .../Bitwarden/Resources/AppResources.mr.resx | 2717 ++++ .../Bitwarden/Resources/AppResources.my.resx | 2717 ++++ .../Bitwarden/Resources/AppResources.nb.resx | 2717 ++++ .../Bitwarden/Resources/AppResources.ne.resx | 2717 ++++ .../Resources/AppResources.nl.Designer.cs | 0 .../Bitwarden/Resources/AppResources.nl.resx | 2716 ++++ .../Bitwarden/Resources/AppResources.nn.resx | 2717 ++++ .../Bitwarden/Resources/AppResources.or.resx | 2717 ++++ .../Resources/AppResources.pl.Designer.cs | 0 .../Bitwarden/Resources/AppResources.pl.resx | 2716 ++++ .../Resources/AppResources.pt-BR.Designer.cs | 0 .../Resources/AppResources.pt-BR.resx | 2717 ++++ .../Resources/AppResources.pt-PT.Designer.cs | 0 .../Resources/AppResources.pt-PT.resx | 2715 ++++ .../Bitwarden/Resources/AppResources.resx | 2717 ++++ .../Resources/AppResources.ro.Designer.cs | 0 .../Bitwarden/Resources/AppResources.ro.resx | 2716 ++++ .../Resources/AppResources.ru.Designer.cs | 0 .../Bitwarden/Resources/AppResources.ru.resx | 2716 ++++ .../Bitwarden/Resources/AppResources.si.resx | 2717 ++++ .../Resources/AppResources.sk.Designer.cs | 0 .../Bitwarden/Resources/AppResources.sk.resx | 2716 ++++ .../Bitwarden/Resources/AppResources.sl.resx | 2716 ++++ .../Bitwarden/Resources/AppResources.sr.resx | 2718 ++++ .../Resources/AppResources.sv.Designer.cs | 0 .../Bitwarden/Resources/AppResources.sv.resx | 2718 ++++ .../Bitwarden/Resources/AppResources.ta.resx | 2717 ++++ .../Bitwarden/Resources/AppResources.te.resx | 2717 ++++ .../Resources/AppResources.th.Designer.cs | 0 .../Bitwarden/Resources/AppResources.th.resx | 2724 ++++ .../Resources/AppResources.tr.Designer.cs | 0 .../Bitwarden/Resources/AppResources.tr.resx | 2715 ++++ .../Resources/AppResources.uk.Designer.cs | 0 .../Bitwarden/Resources/AppResources.uk.resx | 2716 ++++ .../Resources/AppResources.vi.Designer.cs | 0 .../Bitwarden/Resources/AppResources.vi.resx | 2716 ++++ .../AppResources.zh-Hans.Designer.cs | 0 .../Resources/AppResources.zh-Hans.resx | 2716 ++++ .../AppResources.zh-Hant.Designer.cs | 0 .../Resources/AppResources.zh-Hant.resx | 2716 ++++ .../Resources/Fonts/OpenSans-Regular.ttf | Bin 0 -> 107144 bytes .../Resources/Fonts/OpenSans-Semibold.ttf | Bin 0 -> 111072 bytes .../Bitwarden/Resources/Images/dotnet_bot.svg | 95 + .../Bitwarden/Resources/Raw/AboutAssets.txt | 17 + .../Bitwarden/Resources/Splash/splash.svg | 9 + .../Bitwarden/Resources/Styles/Colors.xaml | 44 + .../Bitwarden/Resources/Styles/Styles.xaml | 406 + .../Services/BaseWatchDeviceService.cs | 142 + .../Bitwarden/Services/DeepLinkContext.cs | 30 + .../MobileBroadcasterMessagingService.cs | 21 + .../Bitwarden/Services/MobileI18nService.cs | 153 + .../Services/MobilePasswordRepromptService.cs | 50 + .../Services/MobilePlatformUtilsService.cs | 283 + .../Services/MobileStorageService.cs | 84 + .../NoopPushNotificationListenerService.cs | 43 + .../Services/NoopPushNotificationService.cs | 36 + .../Services/PreferencesStorageService.cs | 149 + .../PushNotificationListenerService.cs | 273 + .../Services/SecureStorageService.cs | 56 + src/Maui/Bitwarden/Styles/Android.xaml | 370 + src/Maui/Bitwarden/Styles/Android.xaml.cs | 13 + src/Maui/Bitwarden/Styles/Base.xaml | 592 + src/Maui/Bitwarden/Styles/Base.xaml.cs | 13 + src/Maui/Bitwarden/Styles/Black.xaml | 76 + src/Maui/Bitwarden/Styles/Black.xaml.cs | 13 + src/Maui/Bitwarden/Styles/Dark.xaml | 76 + src/Maui/Bitwarden/Styles/Dark.xaml.cs | 13 + .../Bitwarden/Styles/IThemeDirtablePage.cs | 15 + .../Styles/IThemeResourceDictionary.cs | 6 + src/Maui/Bitwarden/Styles/Light.xaml | 76 + src/Maui/Bitwarden/Styles/Light.xaml.cs | 13 + src/Maui/Bitwarden/Styles/Nord.xaml | 76 + src/Maui/Bitwarden/Styles/Nord.xaml.cs | 13 + src/Maui/Bitwarden/Styles/SolarizedDark.xaml | 76 + .../Bitwarden/Styles/SolarizedDark.xaml.cs | 13 + src/Maui/Bitwarden/Styles/Variables.xaml | 6 + src/Maui/Bitwarden/Styles/Variables.xaml.cs | 13 + src/Maui/Bitwarden/Styles/iOS.xaml | 394 + src/Maui/Bitwarden/Styles/iOS.xaml.cs | 13 + .../AccountManagement/AccountsManager.cs | 267 + .../AccountManagement/LockNavigationParams.cs | 14 + .../LoginNavigationParams.cs | 14 + src/Maui/Bitwarden/Utilities/AppHelpers.cs | 602 + src/Maui/Bitwarden/Utilities/AppSetup.cs | 26 + src/Maui/Bitwarden/Utilities/AsyncCommand.cs | 70 + .../Automation/AutomationIdsHelper.cs | 23 + .../Utilities/Automation/SuffixType.cs | 13 + .../BoxRowVsBoxRowInputPaddingConverter.cs | 25 + .../Utilities/ColoredPasswordConverter.cs | 29 + .../Bitwarden/Utilities/DateTimeConverter.cs | 41 + src/Maui/Bitwarden/Utilities/EnumHelper.cs | 33 + .../Utilities/GeneratedValueFormatter.cs | 126 + src/Maui/Bitwarden/Utilities/I18nExtension.cs | 38 + .../Utilities/IPasswordPromptable.cs | 9 + .../Bitwarden/Utilities/IconGlyphConverter.cs | 33 + .../Utilities/IconGlyphExtensions.cs | 64 + .../Bitwarden/Utilities/IconImageConverter.cs | 109 + .../Utilities/InverseBoolConverter.cs | 25 + .../Bitwarden/Utilities/IsNotNullConverter.cs | 25 + .../Bitwarden/Utilities/IsNullConverter.cs | 25 + .../Utilities/LocalizableEnumConverter.cs | 24 + .../Bitwarden/Utilities/PageExtensions.cs | 54 + .../Bitwarden/Utilities/PermissionManager.cs | 20 + .../Utilities/ProgressBarExtensions.cs | 26 + .../Prompts/ValidatablePromptConfig.cs | 29 + .../Utilities/SendIconGlyphConverter.cs | 38 + .../Utilities/StringHasValueConverter.cs | 32 + src/Maui/Bitwarden/Utilities/ThemeManager.cs | 198 + src/Maui/Bitwarden/Utilities/TimerTask.cs | 73 + src/Maui/Bitwarden/Utilities/TotpHelper.cs | 58 + .../Bitwarden/Utilities/UpperCaseConverter.cs | 30 + .../VerificationActionsFlowHelper.cs | 157 + 1088 files changed, 261897 insertions(+), 458 deletions(-) create mode 100644 global.json create mode 100644 src/Maui/Bitwarden/Abstractions/IAccountsManager.cs create mode 100644 src/Maui/Bitwarden/Abstractions/IAccountsManagerHost.cs create mode 100644 src/Maui/Bitwarden/Abstractions/IDeepLinkContext.cs create mode 100644 src/Maui/Bitwarden/Abstractions/IDeviceActionService.cs create mode 100644 src/Maui/Bitwarden/Abstractions/ILocalizeService.cs create mode 100644 src/Maui/Bitwarden/Abstractions/IPasswordRepromptService.cs create mode 100644 src/Maui/Bitwarden/Abstractions/IPushNotificationListenerService.cs create mode 100644 src/Maui/Bitwarden/Abstractions/IPushNotificationService.cs create mode 100644 src/Maui/Bitwarden/App.xaml create mode 100644 src/Maui/Bitwarden/App.xaml.cs create mode 100644 src/Maui/Bitwarden/Behaviors/EditorPreventAutoBottomScrollingOnFocusedBehavior.cs create mode 100644 src/Maui/Bitwarden/Bitwarden.csproj create mode 100644 src/Maui/Bitwarden/Bitwarden.sln create mode 100644 src/Maui/Bitwarden/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml create mode 100644 src/Maui/Bitwarden/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs create mode 100644 src/Maui/Bitwarden/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs create mode 100644 src/Maui/Bitwarden/Controls/AccountViewCell/AccountViewCell.xaml create mode 100644 src/Maui/Bitwarden/Controls/AccountViewCell/AccountViewCell.xaml.cs create mode 100644 src/Maui/Bitwarden/Controls/AccountViewCell/AccountViewCellViewModel.cs create mode 100644 src/Maui/Bitwarden/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml create mode 100644 src/Maui/Bitwarden/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml.cs create mode 100644 src/Maui/Bitwarden/Controls/AvatarImageSource.cs create mode 100644 src/Maui/Bitwarden/Controls/AvatarImageSourcePool.cs create mode 100644 src/Maui/Bitwarden/Controls/CipherViewCell/CipherViewCell.xaml create mode 100644 src/Maui/Bitwarden/Controls/CipherViewCell/CipherViewCell.xaml.cs create mode 100644 src/Maui/Bitwarden/Controls/CipherViewCell/CipherViewCellViewModel.cs create mode 100644 src/Maui/Bitwarden/Controls/CircularProgressbarView.cs create mode 100644 src/Maui/Bitwarden/Controls/CustomLabel.cs create mode 100644 src/Maui/Bitwarden/Controls/DateTime/DateTimePicker.xaml create mode 100644 src/Maui/Bitwarden/Controls/DateTime/DateTimePicker.xaml.cs create mode 100644 src/Maui/Bitwarden/Controls/DateTime/DateTimeViewModel.cs create mode 100644 src/Maui/Bitwarden/Controls/ExtendedCollectionView.cs create mode 100644 src/Maui/Bitwarden/Controls/ExtendedDatePicker.cs create mode 100644 src/Maui/Bitwarden/Controls/ExtendedGrid.cs create mode 100644 src/Maui/Bitwarden/Controls/ExtendedSearchBar.cs create mode 100644 src/Maui/Bitwarden/Controls/ExtendedSlider.cs create mode 100644 src/Maui/Bitwarden/Controls/ExtendedStackLayout.cs create mode 100644 src/Maui/Bitwarden/Controls/ExtendedStepper.cs create mode 100644 src/Maui/Bitwarden/Controls/ExtendedTimePicker.cs create mode 100644 src/Maui/Bitwarden/Controls/ExtendedToolbarItem.cs create mode 100644 src/Maui/Bitwarden/Controls/HybridWebView.cs create mode 100644 src/Maui/Bitwarden/Controls/IconButton.cs create mode 100644 src/Maui/Bitwarden/Controls/IconLabel.cs create mode 100644 src/Maui/Bitwarden/Controls/IconLabelButton/IconLabelButton.xaml create mode 100644 src/Maui/Bitwarden/Controls/IconLabelButton/IconLabelButton.xaml.cs create mode 100644 src/Maui/Bitwarden/Controls/MiButton.cs create mode 100644 src/Maui/Bitwarden/Controls/MiLabel.cs create mode 100644 src/Maui/Bitwarden/Controls/MonoEntry.cs create mode 100644 src/Maui/Bitwarden/Controls/MonoLabel.cs create mode 100644 src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/IPasswordStrengthable.cs create mode 100644 src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/PasswordStrengthCategory.cs create mode 100644 src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/PasswordStrengthProgressBar.xaml create mode 100644 src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/PasswordStrengthProgressBar.xaml.cs create mode 100644 src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/PasswordStrengthViewModel.cs create mode 100644 src/Maui/Bitwarden/Controls/RepeaterView.cs create mode 100644 src/Maui/Bitwarden/Controls/SelectableLabel.cs create mode 100644 src/Maui/Bitwarden/Controls/SendViewCell/SendViewCell.xaml create mode 100644 src/Maui/Bitwarden/Controls/SendViewCell/SendViewCell.xaml.cs create mode 100644 src/Maui/Bitwarden/Controls/SendViewCell/SendViewCellViewModel.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IApiService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IAppIdService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IAuditService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IAuthService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IAutofillHandler.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IAzureFileUpoadService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IBiometricService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IBroadcasterService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/ICipherService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IClipboardService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/ICollectionService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IConditionedAwaiterManager.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IConfigService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/ICryptoFunctionService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/ICryptoPrimitiveService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/ICryptoService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IEnvironmentService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IEventService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IExportService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IFileService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IFileUploadService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IFolderService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/II18nService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IKeyConnectorService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/ILogger.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IMessagingService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/INativeLogService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IOrganizationService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IPasswordGenerationService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IPlatformUtilsService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IPolicyService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/ISearchService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/ISendService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/ISettingsService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IStateMigrationService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IStateService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IStorageMediatorService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IStorageService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/ISyncService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/ISynchronousStorageService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/ITokenService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/ITotpService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IUserVerificationService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IUsernameGenerationService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IVaultTimeoutService.cs create mode 100644 src/Maui/Bitwarden/Core/Abstractions/IWatchDeviceService.cs create mode 100644 src/Maui/Bitwarden/Core/Attributes/LocalizableEnumAttribute.cs create mode 100644 src/Maui/Bitwarden/Core/BitwardenIcons.cs create mode 100644 src/Maui/Bitwarden/Core/Constants.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/AuthenticationStatus.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/CipherRepromptType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/CipherType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/ClientType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/CryptoHashAlgorithm.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/DeviceType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/EncryptionType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/EventType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/FieldType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/FileUploadType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/ForwardedEmailServiceType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/GeneratorType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/HashPurpose.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/HdkfAlgorithm.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/KdfType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/LinkedIdType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/NavigationTarget.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/NotificationType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/OrganizationUserStatusType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/OrganizationUserType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/PaymentMethodType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/PlanType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/PolicyType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/SecureNoteType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/SendType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/StorageLocation.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/TransactionType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/TwoFactorProviderType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/UriMatchType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/UsernameEmailType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/UsernameType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/VaultTimeoutAction.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/VerificationType.cs create mode 100644 src/Maui/Bitwarden/Core/Enums/WatchState.cs create mode 100644 src/Maui/Bitwarden/Core/Exceptions/ApiException.cs create mode 100644 src/Maui/Bitwarden/Core/Exceptions/ForwardedEmailInvalidSecretException.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Api/CardApi.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Api/Fido2KeyApi.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Api/FieldApi.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Api/IdentityApi.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Api/LoginApi.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Api/LoginUriApi.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Api/SecureNoteApi.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Api/SendFileApi.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Api/SendTextApi.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/AttachmentData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/CardData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/CipherData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/CollectionData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/Data.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/EnvironmentUrlData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/EventData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/Fido2KeyData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/FieldData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/FolderData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/IdentityData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/LoginData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/LoginUriData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/OrganizationData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/PasswordHistoryData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/Permissions.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/PolicyData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/PreviousPageInfo.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/SecureNoteData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/SendData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/SendFileData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Data/SendTextData.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/Account.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/Attachment.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/AuthResult.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/Card.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/Cipher.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/Collection.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/Domain.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/EncByteArray.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/EncString.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/EnvironmentUrls.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/Fido2Key.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/Field.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/Folder.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/ForcePasswordResetReason.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/GeneratedPasswordHistory.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/ITreeNodeObject.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/Identity.cs create mode 100755 src/Maui/Bitwarden/Core/Models/Domain/KdfConfiguration.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/Login.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/LoginUri.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/MasterPasswordPolicyOptions.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/Message.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/Organization.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/PasswordGenerationOptions.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/PasswordGeneratorPolicyOptions.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/PasswordHistory.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/Policy.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/ResetPasswordPolicyOptions.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/SecureNote.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/Send.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/SendFile.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/SendText.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/State.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/StorageOptions.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/SymmetricCryptoKey.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/TreeNode.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/TwoFactorProvider.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Domain/UsernameGenerationOptions.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Export/Card.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Export/Cipher.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Export/CipherWithId.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Export/Collection.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Export/CollectionWithId.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Export/Field.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Export/Folder.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Export/FolderWithId.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Export/Identity.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Export/Login.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Export/LoginUri.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Export/SecureNote.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/AttachmentRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/CipherCollectionsRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/CipherCreateRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/CipherRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/CipherShareRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/DeleteAccountRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/DeviceRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/DeviceTokenRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/EventRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/FolderRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/KeyConnectorUserKeyRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/KeysRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/OrganizationSsoDomainDetailsRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/PasswordHintRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/PasswordHistoryRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/PasswordRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/PasswordVerificationRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/PasswordlessCreateLoginRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/PasswordlessLoginRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/PreloginRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/RegisterRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/SendRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/SetKeyConnectorKeyRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/SetPasswordRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/TokenRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/TwoFactorEmailRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/UpdateTempPasswordRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Request/VerifyOTPRequest.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/AttachmentResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/AttachmentUploadDataReponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/BreachAccountResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/CipherResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/CollectionResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/ConfigResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/DomainsResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/ErrorResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/FolderResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/GlobalDomainResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/IdentityCaptchaResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/IdentityResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/IdentityTokenResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/IdentityTwoFactorResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/KeyConnectorUserKeyResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/NotificationResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/OrganizationAutoEnrollStatusResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/OrganizationDomainSsoDetailsResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/OrganizationKeysResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/PasswordHistoryResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/PasswordlessLoginResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/PolicyResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/PreloginResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/ProfileOrganizationResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/ProfileResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/SendFileUploadDataResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/SendResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/SsoPrevalidateResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/SyncResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/Response/VerifyMasterPasswordResponse.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/AccountView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/AttachmentView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/CardView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/CipherView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/CollectionView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/Fido2KeyView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/FieldView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/FolderView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/ILaunchableView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/IdentityView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/ItemView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/LoginUriView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/LoginView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/PasswordHistoryView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/SecureNoteView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/SendFileView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/SendTextView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/SendView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/SimpleCipherView.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/View.cs create mode 100644 src/Maui/Bitwarden/Core/Models/View/WatchDTO.cs create mode 100644 src/Maui/Bitwarden/Core/Resources/eff_long_word_list.txt create mode 100644 src/Maui/Bitwarden/Core/Resources/public_suffix_list.dat create mode 100644 src/Maui/Bitwarden/Core/Services/ApiService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/AppIdService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/AuditService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/AuthService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/AzureFileUploadService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/BitwardenFileUploadService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/BroadcasterService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/CipherService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/CollectionService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/ConditionedAwaiterManager.cs create mode 100644 src/Maui/Bitwarden/Core/Services/ConfigService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/ConsoleLogService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/CryptoService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/EmailForwarders/AnonAddyForwarder.cs create mode 100644 src/Maui/Bitwarden/Core/Services/EmailForwarders/BaseForwarder.cs create mode 100644 src/Maui/Bitwarden/Core/Services/EmailForwarders/DuckDuckGoForwarder.cs create mode 100644 src/Maui/Bitwarden/Core/Services/EmailForwarders/FastmailForwarder.cs create mode 100644 src/Maui/Bitwarden/Core/Services/EmailForwarders/FirefoxRelayForwarder.cs create mode 100644 src/Maui/Bitwarden/Core/Services/EmailForwarders/ForwarderOptions.cs create mode 100644 src/Maui/Bitwarden/Core/Services/EmailForwarders/SimpleLoginForwarder.cs create mode 100644 src/Maui/Bitwarden/Core/Services/EnvironmentService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/EventService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/ExportService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/FileUploadService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/FolderService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/InMemoryStorageService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/KeyConnectorService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/LiteDbStorageService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/Logging/DebugLogger.cs create mode 100644 src/Maui/Bitwarden/Core/Services/Logging/Logger.cs create mode 100644 src/Maui/Bitwarden/Core/Services/Logging/LoggerHelper.cs create mode 100644 src/Maui/Bitwarden/Core/Services/Logging/StubLogger.cs create mode 100644 src/Maui/Bitwarden/Core/Services/OrganizationService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/PasswordGenerationService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/PclCryptoFunctionService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/PolicyService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/SearchService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/SendService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/SettingsService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/StateMigrationService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/StateService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/StorageMediatorService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/SyncService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/TokenService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/TotpService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/UserVerificationService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/UsernameGenerationService.cs create mode 100644 src/Maui/Bitwarden/Core/Services/VaultTimeoutService.cs create mode 100644 src/Maui/Bitwarden/Core/Utilities/AccountsManagerMessageCommands.cs create mode 100644 src/Maui/Bitwarden/Core/Utilities/Base32.cs create mode 100644 src/Maui/Bitwarden/Core/Utilities/CipherTypeExtensions.cs create mode 100644 src/Maui/Bitwarden/Core/Utilities/CoreHelpers.cs create mode 100644 src/Maui/Bitwarden/Core/Utilities/DomainName.cs create mode 100644 src/Maui/Bitwarden/Core/Utilities/EEFLongWordList.cs create mode 100644 src/Maui/Bitwarden/Core/Utilities/ExtendedObservableCollection.cs create mode 100644 src/Maui/Bitwarden/Core/Utilities/ExtendedViewModel.cs create mode 100644 src/Maui/Bitwarden/Core/Utilities/LazyResolve.cs create mode 100644 src/Maui/Bitwarden/Core/Utilities/OtpData.cs create mode 100644 src/Maui/Bitwarden/Core/Utilities/ServiceContainer.cs create mode 100644 src/Maui/Bitwarden/Core/Utilities/StringExtensions.cs create mode 100644 src/Maui/Bitwarden/Core/Utilities/TaskExtensions.cs create mode 100644 src/Maui/Bitwarden/Core/Utilities/UriExtensions.cs create mode 100644 src/Maui/Bitwarden/Effects/FabShadowEffect.cs create mode 100644 src/Maui/Bitwarden/Effects/FixedSizeEffect.cs create mode 100644 src/Maui/Bitwarden/Effects/NoEmojiKeyboardEffect.cs create mode 100644 src/Maui/Bitwarden/Effects/RemoveFontPaddingEffect.cs create mode 100644 src/Maui/Bitwarden/Effects/ScrollEnabledEffect.cs create mode 100644 src/Maui/Bitwarden/Effects/ScrollViewContentInsetAdjustmentBehaviorEffect.cs create mode 100644 src/Maui/Bitwarden/Effects/TabBarEffect.cs create mode 100644 src/Maui/Bitwarden/Lists/DataTemplateSelectors/CustomFieldItemTemplateSelector.cs create mode 100644 src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/BooleanCustomFieldItemLayout.xaml create mode 100644 src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/BooleanCustomFieldItemLayout.xaml.cs create mode 100644 src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/HiddenCustomFieldItemLayout.xaml create mode 100644 src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/HiddenCustomFieldItemLayout.xaml.cs create mode 100644 src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/LinkedCustomFieldItemLayout.xaml create mode 100644 src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/LinkedCustomFieldItemLayout.xaml.cs create mode 100644 src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/TextCustomFieldItemLayout.xaml create mode 100644 src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/TextCustomFieldItemLayout.xaml.cs create mode 100644 src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/BaseCustomFieldItemViewModel.cs create mode 100644 src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/BooleanCustomFieldItemViewModel.cs create mode 100644 src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/CustomFieldItemFactory.cs create mode 100644 src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/HiddenCustomFieldItemViewModel.cs create mode 100644 src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/ICustomFieldItemViewModel.cs create mode 100644 src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/LinkedCustomFieldItemViewModel.cs create mode 100644 src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/TextCustomFieldItemViewModel.cs create mode 100644 src/Maui/Bitwarden/MauiProgram.cs create mode 100644 src/Maui/Bitwarden/Models/AppOptions.cs create mode 100644 src/Maui/Bitwarden/Models/DialogDetails.cs create mode 100644 src/Maui/Bitwarden/Models/NotificationData.cs create mode 100644 src/Maui/Bitwarden/Models/PlatformCulture.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/BaseChangePasswordViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/DeleteAccountPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Accounts/DeleteAccountPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/DeleteAccountViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/EnvironmentPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Accounts/EnvironmentPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/EnvironmentPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/HintPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Accounts/HintPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/HintPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/HomePage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Accounts/HomePage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/HomePageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/LockPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Accounts/LockPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/LockPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/LoginPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Accounts/LoginPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/LoginPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/LoginPasswordlessPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Accounts/LoginPasswordlessPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/LoginPasswordlessRequestPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Accounts/LoginPasswordlessRequestPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/LoginPasswordlessRequestViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/LoginPasswordlessViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/LoginSsoPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Accounts/LoginSsoPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/LoginSsoPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/RegisterPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Accounts/RegisterPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/RegisterPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/RemoveMasterPasswordPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Accounts/RemoveMasterPasswordPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/RemoveMasterPasswordPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/SetPasswordPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Accounts/SetPasswordPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/SetPasswordPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/TwoFactorPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Accounts/TwoFactorPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/TwoFactorPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/UpdateTempPasswordPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Accounts/UpdateTempPasswordPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/UpdateTempPasswordPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/VerificationCodePage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Accounts/VerificationCodePage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Accounts/VerificationCodeViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/BaseContentPage.cs create mode 100644 src/Maui/Bitwarden/Pages/BaseViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/CaptchaProtectedViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/CollectionViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Generator/GeneratorHistoryPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Generator/GeneratorHistoryPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Generator/GeneratorHistoryPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Generator/GeneratorPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Generator/GeneratorPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Generator/GeneratorPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Send/SendAddEditPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Send/SendAddEditPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Send/SendAddEditPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Send/SendAddOnlyOptionsView.xaml create mode 100644 src/Maui/Bitwarden/Pages/Send/SendAddOnlyOptionsView.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Send/SendAddOnlyPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Send/SendAddOnlyPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Send/SendGroupingsPage/ISendGroupingsPageListItem.cs create mode 100644 src/Maui/Bitwarden/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Send/SendGroupingsPage/SendGroupingsPageHeaderListItem.cs create mode 100644 src/Maui/Bitwarden/Pages/Send/SendGroupingsPage/SendGroupingsPageListGroup.cs create mode 100644 src/Maui/Bitwarden/Pages/Send/SendGroupingsPage/SendGroupingsPageListItem.cs create mode 100644 src/Maui/Bitwarden/Pages/Send/SendGroupingsPage/SendGroupingsPageListItemSelector.cs create mode 100644 src/Maui/Bitwarden/Pages/Send/SendGroupingsPage/SendGroupingsPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Send/SendsPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Send/SendsPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Send/SendsPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/AutofillPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Settings/AutofillPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/AutofillServicesPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Settings/AutofillServicesPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/AutofillServicesPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/BlockAutofillUrisPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Settings/BlockAutofillUrisPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/BlockAutofillUrisPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/ExportVaultPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Settings/ExportVaultPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/ExportVaultPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/ExtensionPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Settings/ExtensionPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/ExtensionPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/FolderAddEditPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Settings/FolderAddEditPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/FolderAddEditPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/FoldersPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Settings/FoldersPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/FoldersPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/LoginPasswordlessRequestsListPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Settings/LoginPasswordlessRequestsListPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/LoginPasswordlessRequestsListViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/OptionsPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Settings/OptionsPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/OptionsPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/SettingsPage/ISettingsPageListItem.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/SettingsPage/SettingsPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Settings/SettingsPage/SettingsPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/SettingsPage/SettingsPageHeaderListItem.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/SettingsPage/SettingsPageListGroup.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/SettingsPage/SettingsPageListItem.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/SettingsPage/SettingsPageListItemSelector.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/SettingsPage/SettingsPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/SyncPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Settings/SyncPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Settings/SyncPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/TabsPage.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/AttachmentsPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Vault/AttachmentsPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/AttachmentsPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/AutofillCiphersPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/BaseCipherViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/CipherAddEditPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Vault/CipherAddEditPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/CipherAddEditPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/CipherDetailsPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Vault/CipherDetailsPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/CipherDetailsPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/CipherSelectionPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Vault/CipherSelectionPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/CipherSelectionPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/CiphersPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Vault/CiphersPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/CiphersPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/CollectionsPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Vault/CollectionsPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/CollectionsPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/GroupingsPage/GroupingsPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/GroupingsPage/GroupingsPageHeaderListItem.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/GroupingsPage/GroupingsPageListGroup.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/GroupingsPage/GroupingsPageListItem.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/GroupingsPage/GroupingsPageListItemSelector.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/GroupingsPage/GroupingsPageTOTPListItem.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/GroupingsPage/IGroupingsPageListItem.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/OTPCipherSelectionPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/PasswordHistoryPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Vault/PasswordHistoryPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/PasswordHistoryPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/ScanPage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Vault/ScanPage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/ScanPageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/SharePage.xaml create mode 100644 src/Maui/Bitwarden/Pages/Vault/SharePage.xaml.cs create mode 100644 src/Maui/Bitwarden/Pages/Vault/SharePageViewModel.cs create mode 100644 src/Maui/Bitwarden/Pages/VaultFilterViewModel.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/8bit.keystore.enc create mode 100644 src/Maui/Bitwarden/Platforms/Android/Accessibility/AccessibilityActivity.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Accessibility/AccessibilityHelpers.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Accessibility/AccessibilityService.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Accessibility/Browser.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Accessibility/Credentials.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Accessibility/KnownUsernameField.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Accessibility/NodeList.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/AndroidManifest.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Autofill/AutofillConstants.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Autofill/AutofillExternalSelectionActivity.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Autofill/AutofillHelpers.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Autofill/AutofillService.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Autofill/Field.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Autofill/FieldCollection.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Autofill/FilledItem.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Autofill/Parser.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Autofill/SavedItem.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Constants.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Effects/FabShadowEffect.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Effects/FixedSizeEffect.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Effects/NoEmojiKeyboardEffect.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Effects/RemoveFontPaddingEffect.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Effects/TabBarEffect.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/MainActivity.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/MainApplication.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Push/FirebaseMessagingService.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Receivers/ClearClipboardAlarmReceiver.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Receivers/EventUploadReceiver.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Receivers/NotificationDismissReceiver.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Receivers/PackageReplacedReceiver.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Receivers/RestrictionsChangedReceiver.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable-hdpi/logo_legacy.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable-hdpi/logo_white_legacy.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable-hdpi/yubikey.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable-night-v26/splash_screen_round.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable-v23/splash_screen.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable-v23/splash_screen_dark.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable-v26/splash_screen_round.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable-xhdpi/logo_legacy.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable-xhdpi/logo_white_legacy.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable-xhdpi/yubikey.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable-xxhdpi/logo_legacy.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable-xxhdpi/logo_white_legacy.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable-xxhdpi/yubikey.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/card.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/cog_environment.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/cog_settings.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/empty_uris_placeholder.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/empty_uris_placeholder_dark.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/generate.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/ic_launcher_foreground.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/ic_launcher_monochrome.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/ic_notification.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/ic_warning.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/icon.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/id.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/info.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/list_item_bg.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/lock.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/login.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/logo.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/logo_rounded.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/logo_white.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/pencil.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/plus.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/search.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/send.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/shield.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/slider_thumb.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/splash_screen.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/splash_screen_dark.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/drawable/switch_thumb.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/layout/Tabbar.axml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/layout/Toolbar.axml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/layout/autofill_listitem.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/layout/progress_dialog_layout.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/layout/validatable_input_dialog_layout.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/mipmap-hdpi/ic_launcher.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/mipmap-hdpi/ic_launcher_round.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/mipmap-mdpi/ic_launcher.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/mipmap-mdpi/ic_launcher_round.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/mipmap-xhdpi/ic_launcher.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/mipmap-xhdpi/ic_launcher_round.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/values-night/styles.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/values-v30/manifest.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/values/colors.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/values/dimens.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/values/ic_launcher_background.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/values/manifest.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/values/strings.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/values/styles.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/xml/accessibilityservice.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/xml/app_restrictions.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/xml/autofillservice.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/xml/filepaths.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Resources/xml/network_security_config.xml create mode 100644 src/Maui/Bitwarden/Platforms/Android/Services/AndroidLogService.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Services/AndroidPushNotificationService.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Services/AutofillHandler.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Services/BiometricService.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Services/ClipboardService.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Services/CryptoPrimitiveService.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Services/DeviceActionService.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Services/FileService.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Services/LocalizeService.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Services/WatchDeviceService.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Tiles/AutofillTileService.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Tiles/GeneratorTileService.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Tiles/MyVaultTileService.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Utilities/AndroidHelpers.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Utilities/IntentExtensions.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/Utilities/ThemeHelpers.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/WebAuthCallbackActivity.cs create mode 100644 src/Maui/Bitwarden/Platforms/Android/fdroid-keystore.jks.enc create mode 100644 src/Maui/Bitwarden/Platforms/Android/google-services.json create mode 100644 src/Maui/Bitwarden/Platforms/Android/google-services.json.enc create mode 100644 src/Maui/Bitwarden/Platforms/Android/upload-keystore.jks.enc create mode 100644 src/Maui/Bitwarden/Platforms/MacCatalyst/AppDelegate.cs create mode 100644 src/Maui/Bitwarden/Platforms/MacCatalyst/Info.plist create mode 100644 src/Maui/Bitwarden/Platforms/MacCatalyst/Program.cs create mode 100644 src/Maui/Bitwarden/Platforms/Tizen/Main.cs create mode 100644 src/Maui/Bitwarden/Platforms/Tizen/tizen-manifest.xml create mode 100644 src/Maui/Bitwarden/Platforms/Windows/App.xaml create mode 100644 src/Maui/Bitwarden/Platforms/Windows/App.xaml.cs create mode 100644 src/Maui/Bitwarden/Platforms/Windows/Package.appxmanifest create mode 100644 src/Maui/Bitwarden/Platforms/Windows/app.manifest create mode 100644 src/Maui/Bitwarden/Platforms/iOS/AppDelegate.cs create mode 100644 src/Maui/Bitwarden/Platforms/iOS/Info.plist create mode 100644 src/Maui/Bitwarden/Platforms/iOS/Program.cs create mode 100644 src/Maui/Bitwarden/Properties/launchSettings.json create mode 100644 src/Maui/Bitwarden/Resources/AppIcon/appicon.svg create mode 100644 src/Maui/Bitwarden/Resources/AppIcon/appiconfg.svg create mode 100644 src/Maui/Bitwarden/Resources/AppResources.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.af.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.ar.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.az.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.be.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.bg.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.bn.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.bs.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.ca.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.cs.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.cs.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.cy.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.da.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.da.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.de.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.de.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.el.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.en-GB.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.en-IN.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.es.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.es.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.et.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.eu.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.fa.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.fi.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.fi.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.fil.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.fr.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.fr.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.gl.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.he.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.hi.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.hi.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.hr.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.hr.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.hu.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.hu.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.id.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.id.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.it.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.it.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.ja.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.ja.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.ka.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.kn.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.ko.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.lt.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.lv.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.ml.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.mr.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.my.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.nb.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.ne.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.nl.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.nl.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.nn.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.or.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.pl.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.pl.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.pt-BR.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.pt-BR.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.pt-PT.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.pt-PT.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.ro.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.ro.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.ru.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.ru.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.si.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.sk.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.sk.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.sl.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.sr.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.sv.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.sv.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.ta.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.te.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.th.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.th.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.tr.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.tr.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.uk.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.uk.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.vi.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.vi.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.zh-Hans.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.zh-Hans.resx create mode 100644 src/Maui/Bitwarden/Resources/AppResources.zh-Hant.Designer.cs create mode 100644 src/Maui/Bitwarden/Resources/AppResources.zh-Hant.resx create mode 100644 src/Maui/Bitwarden/Resources/Fonts/OpenSans-Regular.ttf create mode 100644 src/Maui/Bitwarden/Resources/Fonts/OpenSans-Semibold.ttf create mode 100644 src/Maui/Bitwarden/Resources/Images/dotnet_bot.svg create mode 100644 src/Maui/Bitwarden/Resources/Raw/AboutAssets.txt create mode 100644 src/Maui/Bitwarden/Resources/Splash/splash.svg create mode 100644 src/Maui/Bitwarden/Resources/Styles/Colors.xaml create mode 100644 src/Maui/Bitwarden/Resources/Styles/Styles.xaml create mode 100644 src/Maui/Bitwarden/Services/BaseWatchDeviceService.cs create mode 100644 src/Maui/Bitwarden/Services/DeepLinkContext.cs create mode 100644 src/Maui/Bitwarden/Services/MobileBroadcasterMessagingService.cs create mode 100644 src/Maui/Bitwarden/Services/MobileI18nService.cs create mode 100644 src/Maui/Bitwarden/Services/MobilePasswordRepromptService.cs create mode 100644 src/Maui/Bitwarden/Services/MobilePlatformUtilsService.cs create mode 100644 src/Maui/Bitwarden/Services/MobileStorageService.cs create mode 100644 src/Maui/Bitwarden/Services/NoopPushNotificationListenerService.cs create mode 100644 src/Maui/Bitwarden/Services/NoopPushNotificationService.cs create mode 100644 src/Maui/Bitwarden/Services/PreferencesStorageService.cs create mode 100644 src/Maui/Bitwarden/Services/PushNotificationListenerService.cs create mode 100644 src/Maui/Bitwarden/Services/SecureStorageService.cs create mode 100644 src/Maui/Bitwarden/Styles/Android.xaml create mode 100644 src/Maui/Bitwarden/Styles/Android.xaml.cs create mode 100644 src/Maui/Bitwarden/Styles/Base.xaml create mode 100644 src/Maui/Bitwarden/Styles/Base.xaml.cs create mode 100644 src/Maui/Bitwarden/Styles/Black.xaml create mode 100644 src/Maui/Bitwarden/Styles/Black.xaml.cs create mode 100644 src/Maui/Bitwarden/Styles/Dark.xaml create mode 100644 src/Maui/Bitwarden/Styles/Dark.xaml.cs create mode 100644 src/Maui/Bitwarden/Styles/IThemeDirtablePage.cs create mode 100644 src/Maui/Bitwarden/Styles/IThemeResourceDictionary.cs create mode 100644 src/Maui/Bitwarden/Styles/Light.xaml create mode 100644 src/Maui/Bitwarden/Styles/Light.xaml.cs create mode 100644 src/Maui/Bitwarden/Styles/Nord.xaml create mode 100644 src/Maui/Bitwarden/Styles/Nord.xaml.cs create mode 100644 src/Maui/Bitwarden/Styles/SolarizedDark.xaml create mode 100644 src/Maui/Bitwarden/Styles/SolarizedDark.xaml.cs create mode 100644 src/Maui/Bitwarden/Styles/Variables.xaml create mode 100644 src/Maui/Bitwarden/Styles/Variables.xaml.cs create mode 100644 src/Maui/Bitwarden/Styles/iOS.xaml create mode 100644 src/Maui/Bitwarden/Styles/iOS.xaml.cs create mode 100644 src/Maui/Bitwarden/Utilities/AccountManagement/AccountsManager.cs create mode 100644 src/Maui/Bitwarden/Utilities/AccountManagement/LockNavigationParams.cs create mode 100644 src/Maui/Bitwarden/Utilities/AccountManagement/LoginNavigationParams.cs create mode 100644 src/Maui/Bitwarden/Utilities/AppHelpers.cs create mode 100644 src/Maui/Bitwarden/Utilities/AppSetup.cs create mode 100644 src/Maui/Bitwarden/Utilities/AsyncCommand.cs create mode 100644 src/Maui/Bitwarden/Utilities/Automation/AutomationIdsHelper.cs create mode 100644 src/Maui/Bitwarden/Utilities/Automation/SuffixType.cs create mode 100644 src/Maui/Bitwarden/Utilities/BoxRowVsBoxRowInputPaddingConverter.cs create mode 100644 src/Maui/Bitwarden/Utilities/ColoredPasswordConverter.cs create mode 100644 src/Maui/Bitwarden/Utilities/DateTimeConverter.cs create mode 100644 src/Maui/Bitwarden/Utilities/EnumHelper.cs create mode 100644 src/Maui/Bitwarden/Utilities/GeneratedValueFormatter.cs create mode 100644 src/Maui/Bitwarden/Utilities/I18nExtension.cs create mode 100644 src/Maui/Bitwarden/Utilities/IPasswordPromptable.cs create mode 100644 src/Maui/Bitwarden/Utilities/IconGlyphConverter.cs create mode 100644 src/Maui/Bitwarden/Utilities/IconGlyphExtensions.cs create mode 100644 src/Maui/Bitwarden/Utilities/IconImageConverter.cs create mode 100644 src/Maui/Bitwarden/Utilities/InverseBoolConverter.cs create mode 100644 src/Maui/Bitwarden/Utilities/IsNotNullConverter.cs create mode 100644 src/Maui/Bitwarden/Utilities/IsNullConverter.cs create mode 100644 src/Maui/Bitwarden/Utilities/LocalizableEnumConverter.cs create mode 100644 src/Maui/Bitwarden/Utilities/PageExtensions.cs create mode 100644 src/Maui/Bitwarden/Utilities/PermissionManager.cs create mode 100644 src/Maui/Bitwarden/Utilities/ProgressBarExtensions.cs create mode 100644 src/Maui/Bitwarden/Utilities/Prompts/ValidatablePromptConfig.cs create mode 100644 src/Maui/Bitwarden/Utilities/SendIconGlyphConverter.cs create mode 100644 src/Maui/Bitwarden/Utilities/StringHasValueConverter.cs create mode 100644 src/Maui/Bitwarden/Utilities/ThemeManager.cs create mode 100644 src/Maui/Bitwarden/Utilities/TimerTask.cs create mode 100644 src/Maui/Bitwarden/Utilities/TotpHelper.cs create mode 100644 src/Maui/Bitwarden/Utilities/UpperCaseConverter.cs create mode 100644 src/Maui/Bitwarden/Utilities/VerificationActionsFlowHelper.cs diff --git a/global.json b/global.json new file mode 100644 index 000000000..de544e70c --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "7.0.306" + } +} diff --git a/src/App/App.csproj b/src/App/App.csproj index a39ef90af..7742b0589 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -1,33 +1,28 @@  - - netstandard2.1 Bit.App BitwardenApp Debug;Release;FDroid + net7.0-android;net7.0-ios + True + Library + enable + true - - - pdbonly - true - - - - - - + + + + - - EnvironmentPage.xaml @@ -130,7 +125,6 @@ LoginPasswordlessRequestPage.xaml - @@ -148,11 +142,9 @@ - - Black.xaml @@ -176,7 +168,6 @@ Android.xaml - AppResources.cs.resx @@ -314,7 +305,6 @@ True - AppResources.cs.Designer.cs @@ -425,7 +415,6 @@ ResXFileCodeGenerator - @@ -445,4 +434,4 @@ - + \ No newline at end of file diff --git a/src/App/App.xaml b/src/App/App.xaml index c21ed3905..ab6339d82 100644 --- a/src/App/App.xaml +++ b/src/App/App.xaml @@ -1,5 +1,5 @@ - - + diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs index aa839370b..7df514569 100644 --- a/src/App/App.xaml.cs +++ b/src/App/App.xaml.cs @@ -15,8 +15,9 @@ using Bit.Core.Models.Data; using Bit.Core.Models.Response; using Bit.Core.Services; using Bit.Core.Utilities; -using Xamarin.Forms; -using Xamarin.Forms.Xaml; +using Microsoft.Maui.Controls.Xaml; +using Microsoft.Maui.Controls; +using Microsoft.Maui; [assembly: XamlCompilation(XamlCompilationOptions.Compile)] namespace Bit.App @@ -93,6 +94,7 @@ namespace Bit.App } else if (message.Command == "resumed") { + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.iOS) { ResumedAsync().FireAndForget(); @@ -100,6 +102,7 @@ namespace Bit.App } else if (message.Command == "slept") { + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.iOS) { await SleptAsync(); @@ -121,7 +124,7 @@ namespace Bit.App Options.OtpData = new OtpData((string)message.Data); } - await Device.InvokeOnMainThreadAsync(async () => + Device.InvokeOnMainThreadAsync(async () => { if (Current.MainPage is TabsPage tabsPage) { @@ -293,6 +296,7 @@ namespace Bit.App { _messagingService.Send(Constants.PasswordlessLoginRequestKey); } + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.Android) { await _vaultTimeoutService.CheckVaultTimeoutAsync(); @@ -308,6 +312,7 @@ namespace Bit.App { System.Diagnostics.Debug.WriteLine("XF App: OnSleep"); _isResumed = false; + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.Android) { var isLocked = await _vaultTimeoutService.IsLockedAsync(); @@ -331,6 +336,7 @@ namespace Bit.App { _messagingService.Send(Constants.PasswordlessLoginRequestKey); } + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.Android) { ResumedAsync().FireAndForget(); @@ -396,6 +402,7 @@ namespace Bit.App private void ClearAutofillUri() { + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.Android && !string.IsNullOrWhiteSpace(Options.Uri)) { Options.Uri = null; @@ -404,6 +411,7 @@ namespace Bit.App private bool SetTabsPageFromAutofill(bool isLocked) { + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.Android && !string.IsNullOrWhiteSpace(Options.Uri) && !Options.FromAutofillFramework) { @@ -452,7 +460,7 @@ namespace Bit.App private void SyncIfNeeded() { - if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None) + if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None) { return; } diff --git a/src/App/Behaviors/EditorPreventAutoBottomScrollingOnFocusedBehavior.cs b/src/App/Behaviors/EditorPreventAutoBottomScrollingOnFocusedBehavior.cs index b00f63737..618f821ce 100644 --- a/src/App/Behaviors/EditorPreventAutoBottomScrollingOnFocusedBehavior.cs +++ b/src/App/Behaviors/EditorPreventAutoBottomScrollingOnFocusedBehavior.cs @@ -1,5 +1,5 @@ -using Xamarin.Essentials; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Behaviors { diff --git a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml index 4dba94dd1..1f6580e3f 100644 --- a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml +++ b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml @@ -1,8 +1,8 @@ - + Device.RuntimePlatform == Device.Android ? 74 : 70; + public int AccountListRowHeight => // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes +Device.RuntimePlatform == Device.Android ? 74 : 70; public bool LongPressAccountEnabled { get; set; } = true; diff --git a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs index f9cd86b73..0153aa15e 100644 --- a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs +++ b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs @@ -7,7 +7,8 @@ using Bit.Core.Abstractions; using Bit.Core.Models.View; using Bit.Core.Utilities; using Xamarin.CommunityToolkit.ObjectModel; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Controls { diff --git a/src/App/Controls/AccountViewCell/AccountViewCell.xaml b/src/App/Controls/AccountViewCell/AccountViewCell.xaml index 9a9f53831..9d270b67a 100644 --- a/src/App/Controls/AccountViewCell/AccountViewCell.xaml +++ b/src/App/Controls/AccountViewCell/AccountViewCell.xaml @@ -1,7 +1,7 @@ - - + - - + + - + + - + + - + - + - + + + + + RememberEmail = !RememberEmail); ContinueCommand = new AsyncCommand(ContinueToLoginStepAsync, allowsMultipleExecutions: false); - CreateAccountCommand = new AsyncCommand(async () => await Device.InvokeOnMainThreadAsync(StartRegisterAction), + CreateAccountCommand = new AsyncCommand(async () => Device.InvokeOnMainThreadAsync(StartRegisterAction), onException: _logger.Exception, allowsMultipleExecutions: false); - CloseCommand = new AsyncCommand(async () => await Device.InvokeOnMainThreadAsync(CloseAction), + CloseCommand = new AsyncCommand(async () => Device.InvokeOnMainThreadAsync(CloseAction), onException: _logger.Exception, allowsMultipleExecutions: false); ShowEnvironmentPickerCommand = new AsyncCommand(ShowEnvironmentPickerAsync, onException: _logger.Exception, allowsMultipleExecutions: false); diff --git a/src/App/Pages/Accounts/LockPage.xaml b/src/App/Pages/Accounts/LockPage.xaml index f34a8b284..396db85a1 100644 --- a/src/App/Pages/Accounts/LockPage.xaml +++ b/src/App/Pages/Accounts/LockPage.xaml @@ -1,6 +1,6 @@ - + Device.BeginInvokeOnMainThread(async () => await UnlockedAsync()); + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.iOS) { ToolbarItems.Add(_moreItem); diff --git a/src/App/Pages/Accounts/LockPageViewModel.cs b/src/App/Pages/Accounts/LockPageViewModel.cs index c56968933..9b5d3437c 100644 --- a/src/App/Pages/Accounts/LockPageViewModel.cs +++ b/src/App/Pages/Accounts/LockPageViewModel.cs @@ -11,8 +11,9 @@ using Bit.Core.Models.Domain; using Bit.Core.Models.Request; using Bit.Core.Services; using Bit.Core.Utilities; -using Xamarin.CommunityToolkit.Helpers; -using Xamarin.Forms; +using CommunityToolkit.Maui.Converters; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Pages { @@ -217,6 +218,7 @@ namespace Bit.App.Pages } BiometricButtonVisible = true; BiometricButtonText = AppResources.UseBiometricsToUnlock; + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.iOS) { var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync(); diff --git a/src/App/Pages/Accounts/LoginPage.xaml b/src/App/Pages/Accounts/LoginPage.xaml index 5f93f9b0c..4fab2e774 100644 --- a/src/App/Pages/Accounts/LoginPage.xaml +++ b/src/App/Pages/Accounts/LoginPage.xaml @@ -1,6 +1,6 @@ - + + + + Microsoft.Maui.ApplicationModel.MainThread.InvokeOnMainThreadAsync(async () => { await _deviceActionService.HideLoadingAsync(); await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage); diff --git a/src/App/Pages/Accounts/LoginPasswordlessViewModel.cs b/src/App/Pages/Accounts/LoginPasswordlessViewModel.cs index 63eb299c8..2f1901f86 100644 --- a/src/App/Pages/Accounts/LoginPasswordlessViewModel.cs +++ b/src/App/Pages/Accounts/LoginPasswordlessViewModel.cs @@ -13,7 +13,8 @@ using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.CommunityToolkit.ObjectModel; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Pages { diff --git a/src/App/Pages/Accounts/LoginSsoPage.xaml b/src/App/Pages/Accounts/LoginSsoPage.xaml index 3c6fd941d..ff3a208b4 100644 --- a/src/App/Pages/Accounts/LoginSsoPage.xaml +++ b/src/App/Pages/Accounts/LoginSsoPage.xaml @@ -1,6 +1,6 @@ - + + + + + Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync()); _vm.CloseAction = async () => await Navigation.PopModalAsync(); DuoWebView = _duoWebView; + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.Android) { ToolbarItems.Remove(_cancelItem); } + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.iOS) { ToolbarItems.Add(_moreItem); diff --git a/src/App/Pages/Accounts/TwoFactorPageViewModel.cs b/src/App/Pages/Accounts/TwoFactorPageViewModel.cs index 486b54f80..b9d99d597 100644 --- a/src/App/Pages/Accounts/TwoFactorPageViewModel.cs +++ b/src/App/Pages/Accounts/TwoFactorPageViewModel.cs @@ -14,8 +14,8 @@ using Bit.Core.Models.Request; using Bit.Core.Utilities; using Newtonsoft.Json; using Xamarin.CommunityToolkit.ObjectModel; -using Xamarin.Essentials; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Pages { @@ -84,7 +84,8 @@ namespace Bit.App.Pages public bool TotpMethod => AuthenticatorMethod || EmailMethod; - public bool ShowTryAgain => (YubikeyMethod && Device.RuntimePlatform == Device.iOS) || Fido2Method; + public bool ShowTryAgain => (YubikeyMethod && // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes +Device.RuntimePlatform == Device.iOS) || Fido2Method; public bool ShowContinue { @@ -98,7 +99,8 @@ namespace Bit.App.Pages set => SetProperty(ref _enableContinue, value); } - public string YubikeyInstruction => Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos : + public string YubikeyInstruction => // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes +Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos : AppResources.YubiKeyInstruction; public TwoFactorProviderType? SelectedProviderType @@ -275,7 +277,7 @@ namespace Bit.App.Pages { return; } - if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None) + if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None) { await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, AppResources.InternetConnectionRequiredTitle, AppResources.Ok); @@ -382,7 +384,7 @@ namespace Bit.App.Pages { return false; } - if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None) + if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None) { await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, AppResources.InternetConnectionRequiredTitle, AppResources.Ok); diff --git a/src/App/Pages/Accounts/UpdateTempPasswordPage.xaml b/src/App/Pages/Accounts/UpdateTempPasswordPage.xaml index f559de05a..911033cad 100644 --- a/src/App/Pages/Accounts/UpdateTempPasswordPage.xaml +++ b/src/App/Pages/Accounts/UpdateTempPasswordPage.xaml @@ -1,6 +1,6 @@ + ().SetUseSafeArea(true); @@ -99,6 +101,7 @@ namespace Bit.App.Pages } } } + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.iOS) { await DoWorkAsync(); diff --git a/src/App/Pages/BaseViewModel.cs b/src/App/Pages/BaseViewModel.cs index 57098f8d0..a1ad04b98 100644 --- a/src/App/Pages/BaseViewModel.cs +++ b/src/App/Pages/BaseViewModel.cs @@ -6,7 +6,8 @@ using Bit.Core.Abstractions; using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core.Utilities; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Pages { @@ -39,7 +40,7 @@ namespace Bit.App.Pages message = apiException.Error.GetSingleMessage(); } - Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () => + Microsoft.Maui.ApplicationModel.MainThread.InvokeOnMainThreadAsync(async () => { await _deviceActionService.Value.HideLoadingAsync(); await _platformUtilsService.Value.ShowDialogAsync(message ?? AppResources.GenericErrorMessage); diff --git a/src/App/Pages/CaptchaProtectedViewModel.cs b/src/App/Pages/CaptchaProtectedViewModel.cs index 2f9969080..f5bba609d 100644 --- a/src/App/Pages/CaptchaProtectedViewModel.cs +++ b/src/App/Pages/CaptchaProtectedViewModel.cs @@ -4,7 +4,6 @@ using Bit.App.Abstractions; using Bit.App.Resources; using Bit.App.Utilities; using Bit.Core.Abstractions; -using Xamarin.Essentials; namespace Bit.App.Pages { diff --git a/src/App/Pages/Generator/GeneratorHistoryPage.xaml b/src/App/Pages/Generator/GeneratorHistoryPage.xaml index 8fbe19930..84bc88f31 100644 --- a/src/App/Pages/Generator/GeneratorHistoryPage.xaml +++ b/src/App/Pages/Generator/GeneratorHistoryPage.xaml @@ -1,6 +1,6 @@ - + + ().SetUpdateMode(UpdateMode.WhenFinished); + _typePicker.On().SetUpdateMode(UpdateMode.WhenFinished); _passwordTypePicker.On().SetUpdateMode(UpdateMode.WhenFinished); _usernameTypePicker.On().SetUpdateMode(UpdateMode.WhenFinished); _serviceTypePicker.On().SetUpdateMode(UpdateMode.WhenFinished); @@ -97,6 +99,7 @@ namespace Bit.App.Pages protected override bool OnBackButtonPressed() { + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.Android && _tabsPage != null) { _tabsPage.ResetToVaultPage(); @@ -116,7 +119,7 @@ namespace Bit.App.Pages if (selection == AppResources.PasswordHistory) { var page = new GeneratorHistoryPage(); - await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page)); + await Navigation.PushModalAsync(new Microsoft.Maui.Controls.NavigationPage(page)); } } @@ -128,7 +131,7 @@ namespace Bit.App.Pages private async void History_Clicked(object sender, EventArgs e) { var page = new GeneratorHistoryPage(); - await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page)); + await Navigation.PushModalAsync(new Microsoft.Maui.Controls.NavigationPage(page)); } private async void LengthSlider_DragCompleted(object sender, EventArgs e) diff --git a/src/App/Pages/Generator/GeneratorPageViewModel.cs b/src/App/Pages/Generator/GeneratorPageViewModel.cs index 935a9f7e0..4c33260d4 100644 --- a/src/App/Pages/Generator/GeneratorPageViewModel.cs +++ b/src/App/Pages/Generator/GeneratorPageViewModel.cs @@ -12,7 +12,8 @@ using Bit.Core.Exceptions; using Bit.Core.Models.Domain; using Bit.Core.Utilities; using Xamarin.CommunityToolkit.ObjectModel; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Pages { diff --git a/src/App/Pages/Send/SendAddEditPage.xaml b/src/App/Pages/Send/SendAddEditPage.xaml index a90ed8cd0..08d1120e6 100644 --- a/src/App/Pages/Send/SendAddEditPage.xaml +++ b/src/App/Pages/Send/SendAddEditPage.xaml @@ -1,8 +1,8 @@ - + + diff --git a/src/App/Pages/Send/SendAddOnlyOptionsView.xaml.cs b/src/App/Pages/Send/SendAddOnlyOptionsView.xaml.cs index 84829ea23..93535cf00 100644 --- a/src/App/Pages/Send/SendAddOnlyOptionsView.xaml.cs +++ b/src/App/Pages/Send/SendAddOnlyOptionsView.xaml.cs @@ -1,8 +1,14 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; using Bit.App.Behaviors; -using Xamarin.CommunityToolkit.UI.Views; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; +using CommunityToolkit.Maui.Converters; +using CommunityToolkit.Maui.ImageSources; +using CommunityToolkit.Maui; +using CommunityToolkit.Maui.Core; +using CommunityToolkit.Maui.Layouts; +using CommunityToolkit.Maui.Views; namespace Bit.App.Pages { diff --git a/src/App/Pages/Send/SendAddOnlyPage.xaml b/src/App/Pages/Send/SendAddOnlyPage.xaml index 844313361..bc5c2a600 100644 --- a/src/App/Pages/Send/SendAddOnlyPage.xaml +++ b/src/App/Pages/Send/SendAddOnlyPage.xaml @@ -1,6 +1,6 @@ - + - + + + + { if (_timerStarted == null || (DateTime.UtcNow - _timerStarted) > _timerMaxLength) diff --git a/src/App/Pages/Settings/BlockAutofillUrisPage.xaml b/src/App/Pages/Settings/BlockAutofillUrisPage.xaml index 8514edcad..1a864ea89 100644 --- a/src/App/Pages/Settings/BlockAutofillUrisPage.xaml +++ b/src/App/Pages/Settings/BlockAutofillUrisPage.xaml @@ -1,5 +1,5 @@ - - + + + + + + + (); PageTitle = AppResources.Options; + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes var iosIos = Device.RuntimePlatform == Device.iOS; ClearClipboardOptions = new List> diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml b/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml index 4976573af..6e9f0e105 100644 --- a/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml +++ b/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml @@ -1,5 +1,5 @@ - - +(); + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.Android) { autofillItems.Add(new SettingsPageListItem @@ -587,6 +591,7 @@ namespace Bit.App.Pages if (_supportsBiometric || _biometric) { var biometricName = AppResources.Biometrics; + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.iOS) { biometricName = _deviceActionService.SupportsFaceBiometric() ? AppResources.FaceID : @@ -641,6 +646,7 @@ namespace Bit.App.Pages }); } } + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.Android) { securityItems.Add(new SettingsPageListItem @@ -651,6 +657,7 @@ namespace Bit.App.Pages }); } var accountItems = new List(); + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.iOS) { accountItems.Add(new SettingsPageListItem @@ -754,7 +761,8 @@ namespace Bit.App.Pages }; // TODO: refactor this - if (Device.RuntimePlatform == Device.Android + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + if (Device.RuntimePlatform == Device.Android || GroupedItems.Any()) { @@ -818,6 +826,7 @@ namespace Bit.App.Pages private bool IncludeLinksWithSubscriptionInfo() { + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.iOS) { return false; diff --git a/src/App/Pages/Settings/SyncPage.xaml b/src/App/Pages/Settings/SyncPage.xaml index 7ad3a4fd6..877a3a017 100644 --- a/src/App/Pages/Settings/SyncPage.xaml +++ b/src/App/Pages/Settings/SyncPage.xaml @@ -1,6 +1,6 @@ - + + SubmitAsync() { - if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None) + if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None) { await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, AppResources.InternetConnectionRequiredTitle); @@ -154,7 +155,8 @@ namespace Bit.App.Pages public async Task ChooseFileAsync() { // Prevent Android from locking if vault timeout set to "immediate" - if (Device.RuntimePlatform == Device.Android) + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + if (Device.RuntimePlatform == Device.Android) { _vaultTimeoutService.DelayLockAndLogoutMs = 60000; } @@ -163,7 +165,7 @@ namespace Bit.App.Pages private async void DeleteAsync(AttachmentView attachment) { - if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None) + if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None) { await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, AppResources.InternetConnectionRequiredTitle); diff --git a/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs b/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs index aade36dd6..5b93ccba6 100644 --- a/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs +++ b/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs @@ -9,7 +9,8 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.View; using Bit.Core.Utilities; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Pages { @@ -91,7 +92,7 @@ namespace Bit.App.Pages { var options = new List { AppResources.Yes }; if (cipher.Type == CipherType.Login && - Xamarin.Essentials.Connectivity.NetworkAccess != Xamarin.Essentials.NetworkAccess.None) + Microsoft.Maui.Networking.Connectivity.NetworkAccess != Microsoft.Maui.Networking.NetworkAccess.None) { options.Add(AppResources.YesAndSave); } diff --git a/src/App/Pages/Vault/CipherAddEditPage.xaml b/src/App/Pages/Vault/CipherAddEditPage.xaml index 453da9146..a1fb16984 100644 --- a/src/App/Pages/Vault/CipherAddEditPage.xaml +++ b/src/App/Pages/Vault/CipherAddEditPage.xaml @@ -1,6 +1,6 @@ - + DeleteAsync() { - if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None) + if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None) { await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, AppResources.InternetConnectionRequiredTitle); diff --git a/src/App/Pages/Vault/CipherDetailsPage.xaml b/src/App/Pages/Vault/CipherDetailsPage.xaml index 8aaf74706..a88866ef8 100644 --- a/src/App/Pages/Vault/CipherDetailsPage.xaml +++ b/src/App/Pages/Vault/CipherDetailsPage.xaml @@ -1,6 +1,6 @@ - + DeleteAsync() { - if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None) + if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None) { await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, AppResources.InternetConnectionRequiredTitle); @@ -403,7 +404,7 @@ namespace Bit.App.Pages { return false; } - if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None) + if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None) { await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, AppResources.InternetConnectionRequiredTitle); @@ -479,7 +480,7 @@ namespace Bit.App.Pages { try { - if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None) + if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None) { await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, AppResources.InternetConnectionRequiredTitle); @@ -504,6 +505,7 @@ namespace Bit.App.Pages var canOpenFile = true; if (!_fileService.CanOpenFile(attachment.FileName)) { + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.iOS) { // iOS is currently hardcoded to always return CanOpenFile == true, but should it ever return false @@ -529,6 +531,7 @@ namespace Bit.App.Pages return; } + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.Android) { if (canOpenFile) diff --git a/src/App/Pages/Vault/CipherSelectionPage.xaml b/src/App/Pages/Vault/CipherSelectionPage.xaml index f71d3a2c2..27fbaec7c 100644 --- a/src/App/Pages/Vault/CipherSelectionPage.xaml +++ b/src/App/Pages/Vault/CipherSelectionPage.xaml @@ -1,7 +1,7 @@ - - + + { AppResources.Autofill }; if (cipher.Type == CipherType.Login && - Xamarin.Essentials.Connectivity.NetworkAccess != Xamarin.Essentials.NetworkAccess.None) + Microsoft.Maui.Networking.Connectivity.NetworkAccess != Microsoft.Maui.Networking.NetworkAccess.None) { options.Add(AppResources.AutofillAndSave); } diff --git a/src/App/Pages/Vault/CollectionsPage.xaml b/src/App/Pages/Vault/CollectionsPage.xaml index 86980c45d..af08b2798 100644 --- a/src/App/Pages/Vault/CollectionsPage.xaml +++ b/src/App/Pages/Vault/CollectionsPage.xaml @@ -1,6 +1,6 @@ - + - + groupedItems) { + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes var uppercaseGroupNames = Device.RuntimePlatform == Device.iOS; _totpTickCts?.Cancel(); if (ShowTotp) @@ -509,7 +513,7 @@ namespace Bit.App.Pages public async Task SyncAsync() { - if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None) + if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None) { await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, AppResources.InternetConnectionRequiredTitle); diff --git a/src/App/Pages/Vault/OTPCipherSelectionPageViewModel.cs b/src/App/Pages/Vault/OTPCipherSelectionPageViewModel.cs index 537823194..be87b878d 100644 --- a/src/App/Pages/Vault/OTPCipherSelectionPageViewModel.cs +++ b/src/App/Pages/Vault/OTPCipherSelectionPageViewModel.cs @@ -6,7 +6,8 @@ using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Utilities; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Pages { diff --git a/src/App/Pages/Vault/PasswordHistoryPage.xaml b/src/App/Pages/Vault/PasswordHistoryPage.xaml index 215686c7b..69d7afe91 100644 --- a/src/App/Pages/Vault/PasswordHistoryPage.xaml +++ b/src/App/Pages/Vault/PasswordHistoryPage.xaml @@ -1,6 +1,6 @@ - + + InitScanner()); + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (Device.RuntimePlatform == Device.Android) { ToolbarItems.RemoveAt(0); diff --git a/src/App/Pages/Vault/ScanPageViewModel.cs b/src/App/Pages/Vault/ScanPageViewModel.cs index 74b2d1a4c..77f83bcfb 100644 --- a/src/App/Pages/Vault/ScanPageViewModel.cs +++ b/src/App/Pages/Vault/ScanPageViewModel.cs @@ -8,8 +8,8 @@ using Bit.Core.Abstractions; using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.CommunityToolkit.ObjectModel; -using Xamarin.Essentials; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Pages { @@ -118,7 +118,7 @@ namespace Bit.App.Pages private void HandleException(Exception ex) { - Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () => + Microsoft.Maui.ApplicationModel.MainThread.InvokeOnMainThreadAsync(async () => { await _deviceActionService.HideLoadingAsync(); await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage); diff --git a/src/App/Pages/Vault/SharePage.xaml b/src/App/Pages/Vault/SharePage.xaml index da484eae3..5fd963db4 100644 --- a/src/App/Pages/Vault/SharePage.xaml +++ b/src/App/Pages/Vault/SharePage.xaml @@ -1,6 +1,6 @@ - + (string key) { var formattedKey = string.Format(KeyFormat, key); - if (!Xamarin.Essentials.Preferences.ContainsKey(formattedKey, _sharedName)) + if (!Microsoft.Maui.Storage.Preferences.ContainsKey(formattedKey, _sharedName)) { return default(T); } @@ -47,37 +47,37 @@ namespace Bit.App.Services var objType = typeof(T); if (objType == typeof(string)) { - var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(string), _sharedName); + var val = Microsoft.Maui.Storage.Preferences.Get(formattedKey, default(string), _sharedName); return (T)(object)val; } else if (objType == typeof(bool) || objType == typeof(bool?)) { - var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(bool), _sharedName); + var val = Microsoft.Maui.Storage.Preferences.Get(formattedKey, default(bool), _sharedName); return ChangeType(val); } else if (objType == typeof(int) || objType == typeof(int?)) { - var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(int), _sharedName); + var val = Microsoft.Maui.Storage.Preferences.Get(formattedKey, default(int), _sharedName); return ChangeType(val); } else if (objType == typeof(long) || objType == typeof(long?)) { - var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(long), _sharedName); + var val = Microsoft.Maui.Storage.Preferences.Get(formattedKey, default(long), _sharedName); return ChangeType(val); } else if (objType == typeof(double) || objType == typeof(double?)) { - var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(double), _sharedName); + var val = Microsoft.Maui.Storage.Preferences.Get(formattedKey, default(double), _sharedName); return ChangeType(val); } else if (objType == typeof(DateTime) || objType == typeof(DateTime?)) { - var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(DateTime), _sharedName); + var val = Microsoft.Maui.Storage.Preferences.Get(formattedKey, default(DateTime), _sharedName); return ChangeType(val); } else { - var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(string), _sharedName); + var val = Microsoft.Maui.Storage.Preferences.Get(formattedKey, default(string), _sharedName); return JsonConvert.DeserializeObject(val, _jsonSettings); } } @@ -94,31 +94,31 @@ namespace Bit.App.Services var objType = typeof(T); if (objType == typeof(string)) { - Xamarin.Essentials.Preferences.Set(formattedKey, obj as string, _sharedName); + Microsoft.Maui.Storage.Preferences.Set(formattedKey, obj as string, _sharedName); } else if (objType == typeof(bool) || objType == typeof(bool?)) { - Xamarin.Essentials.Preferences.Set(formattedKey, (obj as bool?).Value, _sharedName); + Microsoft.Maui.Storage.Preferences.Set(formattedKey, (obj as bool?).Value, _sharedName); } else if (objType == typeof(int) || objType == typeof(int?)) { - Xamarin.Essentials.Preferences.Set(formattedKey, (obj as int?).Value, _sharedName); + Microsoft.Maui.Storage.Preferences.Set(formattedKey, (obj as int?).Value, _sharedName); } else if (objType == typeof(long) || objType == typeof(long?)) { - Xamarin.Essentials.Preferences.Set(formattedKey, (obj as long?).Value, _sharedName); + Microsoft.Maui.Storage.Preferences.Set(formattedKey, (obj as long?).Value, _sharedName); } else if (objType == typeof(double) || objType == typeof(double?)) { - Xamarin.Essentials.Preferences.Set(formattedKey, (obj as double?).Value, _sharedName); + Microsoft.Maui.Storage.Preferences.Set(formattedKey, (obj as double?).Value, _sharedName); } else if (objType == typeof(DateTime) || objType == typeof(DateTime?)) { - Xamarin.Essentials.Preferences.Set(formattedKey, (obj as DateTime?).Value, _sharedName); + Microsoft.Maui.Storage.Preferences.Set(formattedKey, (obj as DateTime?).Value, _sharedName); } else { - Xamarin.Essentials.Preferences.Set(formattedKey, JsonConvert.SerializeObject(obj, _jsonSettings), + Microsoft.Maui.Storage.Preferences.Set(formattedKey, JsonConvert.SerializeObject(obj, _jsonSettings), _sharedName); } } @@ -126,9 +126,9 @@ namespace Bit.App.Services public void Remove(string key) { var formattedKey = string.Format(KeyFormat, key); - if (Xamarin.Essentials.Preferences.ContainsKey(formattedKey, _sharedName)) + if (Microsoft.Maui.Storage.Preferences.ContainsKey(formattedKey, _sharedName)) { - Xamarin.Essentials.Preferences.Remove(formattedKey, _sharedName); + Microsoft.Maui.Storage.Preferences.Remove(formattedKey, _sharedName); } } diff --git a/src/App/Services/PushNotificationListenerService.cs b/src/App/Services/PushNotificationListenerService.cs index b08a43f1c..d4340c2bc 100644 --- a/src/App/Services/PushNotificationListenerService.cs +++ b/src/App/Services/PushNotificationListenerService.cs @@ -17,7 +17,8 @@ using Bit.Core.Services; using Bit.Core.Utilities; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Services { diff --git a/src/App/Services/SecureStorageService.cs b/src/App/Services/SecureStorageService.cs index 1c1534b68..b519c21e7 100644 --- a/src/App/Services/SecureStorageService.cs +++ b/src/App/Services/SecureStorageService.cs @@ -16,7 +16,7 @@ namespace Bit.App.Services public async Task GetAsync(string key) { var formattedKey = string.Format(_keyFormat, key); - var val = await Xamarin.Essentials.SecureStorage.GetAsync(formattedKey); + var val = await Microsoft.Maui.Storage.SecureStorage.GetAsync(formattedKey); if (typeof(T) == typeof(string)) { return (T)(object)val; @@ -37,11 +37,11 @@ namespace Bit.App.Services var formattedKey = string.Format(_keyFormat, key); if (typeof(T) == typeof(string)) { - await Xamarin.Essentials.SecureStorage.SetAsync(formattedKey, obj as string); + await Microsoft.Maui.Storage.SecureStorage.SetAsync(formattedKey, obj as string); } else { - await Xamarin.Essentials.SecureStorage.SetAsync(formattedKey, + await Microsoft.Maui.Storage.SecureStorage.SetAsync(formattedKey, JsonConvert.SerializeObject(obj, _jsonSettings)); } } @@ -49,7 +49,7 @@ namespace Bit.App.Services public Task RemoveAsync(string key) { var formattedKey = string.Format(_keyFormat, key); - Xamarin.Essentials.SecureStorage.Remove(formattedKey); + Microsoft.Maui.Storage.SecureStorage.Remove(formattedKey); return Task.FromResult(0); } } diff --git a/src/App/Styles/Android.xaml b/src/App/Styles/Android.xaml index 51c2e37aa..f1bb54e79 100644 --- a/src/App/Styles/Android.xaml +++ b/src/App/Styles/Android.xaml @@ -1,5 +1,5 @@ - - + diff --git a/src/App/Styles/Android.xaml.cs b/src/App/Styles/Android.xaml.cs index a2e9ecfd3..c4c469ccf 100644 --- a/src/App/Styles/Android.xaml.cs +++ b/src/App/Styles/Android.xaml.cs @@ -1,4 +1,5 @@ -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Styles { diff --git a/src/App/Styles/Base.xaml b/src/App/Styles/Base.xaml index 2dac11bf7..9a10c7a1a 100644 --- a/src/App/Styles/Base.xaml +++ b/src/App/Styles/Base.xaml @@ -1,5 +1,5 @@ - - + diff --git a/src/App/Styles/Base.xaml.cs b/src/App/Styles/Base.xaml.cs index 2e74ee5ea..a031d5d77 100644 --- a/src/App/Styles/Base.xaml.cs +++ b/src/App/Styles/Base.xaml.cs @@ -1,4 +1,5 @@ -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Styles { diff --git a/src/App/Styles/Black.xaml b/src/App/Styles/Black.xaml index b2027f114..59d990802 100644 --- a/src/App/Styles/Black.xaml +++ b/src/App/Styles/Black.xaml @@ -1,5 +1,5 @@ - - + #ffffff diff --git a/src/App/Styles/Black.xaml.cs b/src/App/Styles/Black.xaml.cs index b126f1bb5..daf47356a 100644 --- a/src/App/Styles/Black.xaml.cs +++ b/src/App/Styles/Black.xaml.cs @@ -1,4 +1,5 @@ -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Styles { diff --git a/src/App/Styles/Dark.xaml b/src/App/Styles/Dark.xaml index 570e9ca2e..684a2100f 100644 --- a/src/App/Styles/Dark.xaml +++ b/src/App/Styles/Dark.xaml @@ -1,5 +1,5 @@ - - + #ffffff diff --git a/src/App/Styles/Dark.xaml.cs b/src/App/Styles/Dark.xaml.cs index 1a4113ab6..ac21a86b4 100644 --- a/src/App/Styles/Dark.xaml.cs +++ b/src/App/Styles/Dark.xaml.cs @@ -1,4 +1,5 @@ -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Styles { diff --git a/src/App/Styles/Light.xaml b/src/App/Styles/Light.xaml index 59a424953..f1811d28f 100644 --- a/src/App/Styles/Light.xaml +++ b/src/App/Styles/Light.xaml @@ -1,5 +1,5 @@ - - + #000000 diff --git a/src/App/Styles/Light.xaml.cs b/src/App/Styles/Light.xaml.cs index 3ddad1482..c1b454a16 100644 --- a/src/App/Styles/Light.xaml.cs +++ b/src/App/Styles/Light.xaml.cs @@ -1,4 +1,5 @@ -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Styles { diff --git a/src/App/Styles/Nord.xaml b/src/App/Styles/Nord.xaml index d4690c2de..7aaf3952a 100644 --- a/src/App/Styles/Nord.xaml +++ b/src/App/Styles/Nord.xaml @@ -1,5 +1,5 @@ - - + #e5e9f0 diff --git a/src/App/Styles/Nord.xaml.cs b/src/App/Styles/Nord.xaml.cs index 6baf44c3b..3052cf2f7 100644 --- a/src/App/Styles/Nord.xaml.cs +++ b/src/App/Styles/Nord.xaml.cs @@ -1,4 +1,5 @@ -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Styles { diff --git a/src/App/Styles/SolarizedDark.xaml b/src/App/Styles/SolarizedDark.xaml index e8402a0d2..c453c2af0 100644 --- a/src/App/Styles/SolarizedDark.xaml +++ b/src/App/Styles/SolarizedDark.xaml @@ -1,5 +1,5 @@ - - + #eee8d5 diff --git a/src/App/Styles/SolarizedDark.xaml.cs b/src/App/Styles/SolarizedDark.xaml.cs index a8dc19768..153c33dff 100644 --- a/src/App/Styles/SolarizedDark.xaml.cs +++ b/src/App/Styles/SolarizedDark.xaml.cs @@ -1,4 +1,5 @@ -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Styles { diff --git a/src/App/Styles/Variables.xaml b/src/App/Styles/Variables.xaml index 49b463edf..2ee2734ad 100644 --- a/src/App/Styles/Variables.xaml +++ b/src/App/Styles/Variables.xaml @@ -1,5 +1,5 @@ - - + diff --git a/src/App/Styles/Variables.xaml.cs b/src/App/Styles/Variables.xaml.cs index 31582c54d..264d4022a 100644 --- a/src/App/Styles/Variables.xaml.cs +++ b/src/App/Styles/Variables.xaml.cs @@ -1,4 +1,5 @@ -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Styles { diff --git a/src/App/Styles/iOS.xaml b/src/App/Styles/iOS.xaml index b6041bcce..768f9a5f4 100644 --- a/src/App/Styles/iOS.xaml +++ b/src/App/Styles/iOS.xaml @@ -1,5 +1,5 @@ - - + diff --git a/src/App/Styles/iOS.xaml.cs b/src/App/Styles/iOS.xaml.cs index a08433b33..bfec6a2c6 100644 --- a/src/App/Styles/iOS.xaml.cs +++ b/src/App/Styles/iOS.xaml.cs @@ -1,4 +1,5 @@ -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Styles { diff --git a/src/App/Utilities/AccountManagement/AccountsManager.cs b/src/App/Utilities/AccountManagement/AccountsManager.cs index 919070359..c80a62488 100644 --- a/src/App/Utilities/AccountManagement/AccountsManager.cs +++ b/src/App/Utilities/AccountManagement/AccountsManager.cs @@ -7,7 +7,8 @@ using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Models.Domain; using Bit.Core.Utilities; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities.AccountManagement { @@ -190,6 +191,7 @@ namespace Bit.App.Utilities.AccountManagement } var autoPromptBiometric = !userInitiated; + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes if (autoPromptBiometric && Device.RuntimePlatform == Device.iOS) { var vaultTimeout = await _stateService.GetVaultTimeoutAsync(); diff --git a/src/App/Utilities/AppHelpers.cs b/src/App/Utilities/AppHelpers.cs index 49086a18b..224cd5b0f 100644 --- a/src/App/Utilities/AppHelpers.cs +++ b/src/App/Utilities/AppHelpers.cs @@ -17,8 +17,8 @@ using Bit.Core.Models.Data; using Bit.Core.Models.View; using Bit.Core.Utilities; using Newtonsoft.Json; -using Xamarin.Essentials; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/BoxRowVsBoxRowInputPaddingConverter.cs b/src/App/Utilities/BoxRowVsBoxRowInputPaddingConverter.cs index 59e9790ec..4a4d81850 100644 --- a/src/App/Utilities/BoxRowVsBoxRowInputPaddingConverter.cs +++ b/src/App/Utilities/BoxRowVsBoxRowInputPaddingConverter.cs @@ -1,5 +1,6 @@ using System; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/ColoredPasswordConverter.cs b/src/App/Utilities/ColoredPasswordConverter.cs index 60222d2fb..ba5f76769 100644 --- a/src/App/Utilities/ColoredPasswordConverter.cs +++ b/src/App/Utilities/ColoredPasswordConverter.cs @@ -1,5 +1,6 @@ using System; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/DateTimeConverter.cs b/src/App/Utilities/DateTimeConverter.cs index cd8f2cec4..5393bd3d1 100644 --- a/src/App/Utilities/DateTimeConverter.cs +++ b/src/App/Utilities/DateTimeConverter.cs @@ -1,7 +1,8 @@ using System; using Bit.App.Abstractions; using Bit.Core.Utilities; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/EnumHelper.cs b/src/App/Utilities/EnumHelper.cs index ef9faf286..edf9b1a88 100644 --- a/src/App/Utilities/EnumHelper.cs +++ b/src/App/Utilities/EnumHelper.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Reflection; using Bit.App.Resources; using Bit.Core.Attributes; -using Xamarin.CommunityToolkit.Helpers; +using CommunityToolkit.Maui.Converters; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/GeneratedValueFormatter.cs b/src/App/Utilities/GeneratedValueFormatter.cs index a718862b2..6f0199ea4 100644 --- a/src/App/Utilities/GeneratedValueFormatter.cs +++ b/src/App/Utilities/GeneratedValueFormatter.cs @@ -1,6 +1,7 @@ using System; using System.Web; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { @@ -38,7 +39,8 @@ namespace Bit.App.Utilities // iOS won't hide the zero-width space char without these div attrs, but Android won't respect // display:inline-block and adds a newline after the password/username. Hence, only iOS gets the div. - if (Device.RuntimePlatform == Device.iOS) + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + if (Device.RuntimePlatform == Device.iOS) { result += "
"; } @@ -112,7 +114,8 @@ namespace Bit.App.Utilities } // Close off iOS div - if (Device.RuntimePlatform == Device.iOS) + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + if (Device.RuntimePlatform == Device.iOS) { result += "
"; } diff --git a/src/App/Utilities/I18nExtension.cs b/src/App/Utilities/I18nExtension.cs index b66040c75..e048d1afe 100644 --- a/src/App/Utilities/I18nExtension.cs +++ b/src/App/Utilities/I18nExtension.cs @@ -1,8 +1,9 @@ using System; using Bit.Core.Abstractions; using Bit.Core.Utilities; -using Xamarin.Forms; -using Xamarin.Forms.Xaml; +using Microsoft.Maui.Controls.Xaml; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/IconGlyphConverter.cs b/src/App/Utilities/IconGlyphConverter.cs index c68c103ac..1c51688fb 100644 --- a/src/App/Utilities/IconGlyphConverter.cs +++ b/src/App/Utilities/IconGlyphConverter.cs @@ -1,7 +1,8 @@ using System; using System.Globalization; using Bit.Core.Models.View; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/IconImageConverter.cs b/src/App/Utilities/IconImageConverter.cs index 87ad2391b..8e1752429 100644 --- a/src/App/Utilities/IconImageConverter.cs +++ b/src/App/Utilities/IconImageConverter.cs @@ -4,7 +4,8 @@ using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Models.View; using Bit.Core.Utilities; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/InverseBoolConverter.cs b/src/App/Utilities/InverseBoolConverter.cs index b0ee98993..27f6a0e8c 100644 --- a/src/App/Utilities/InverseBoolConverter.cs +++ b/src/App/Utilities/InverseBoolConverter.cs @@ -1,5 +1,6 @@ using System; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/IsNotNullConverter.cs b/src/App/Utilities/IsNotNullConverter.cs index 5888ab362..c0d309fee 100644 --- a/src/App/Utilities/IsNotNullConverter.cs +++ b/src/App/Utilities/IsNotNullConverter.cs @@ -1,5 +1,6 @@ using System; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/IsNullConverter.cs b/src/App/Utilities/IsNullConverter.cs index 0d6f85964..6961eb177 100644 --- a/src/App/Utilities/IsNullConverter.cs +++ b/src/App/Utilities/IsNullConverter.cs @@ -1,5 +1,6 @@ using System; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/LocalizableEnumConverter.cs b/src/App/Utilities/LocalizableEnumConverter.cs index 8c1dc287a..1da0cf1ba 100644 --- a/src/App/Utilities/LocalizableEnumConverter.cs +++ b/src/App/Utilities/LocalizableEnumConverter.cs @@ -1,5 +1,6 @@ using System; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/PageExtensions.cs b/src/App/Utilities/PageExtensions.cs index ec64f20ef..fe8132a65 100644 --- a/src/App/Utilities/PageExtensions.cs +++ b/src/App/Utilities/PageExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/PermissionManager.cs b/src/App/Utilities/PermissionManager.cs index 8af2ecff2..99a7de933 100644 --- a/src/App/Utilities/PermissionManager.cs +++ b/src/App/Utilities/PermissionManager.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; -using Xamarin.Essentials; -using static Xamarin.Essentials.Permissions; +using static Microsoft.Maui.ApplicationModel.Permissions; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/ProgressBarExtensions.cs b/src/App/Utilities/ProgressBarExtensions.cs index 7dcf15f85..94d05b511 100644 --- a/src/App/Utilities/ProgressBarExtensions.cs +++ b/src/App/Utilities/ProgressBarExtensions.cs @@ -1,4 +1,5 @@ -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/SendIconGlyphConverter.cs b/src/App/Utilities/SendIconGlyphConverter.cs index 512586768..1cf2fc5f1 100644 --- a/src/App/Utilities/SendIconGlyphConverter.cs +++ b/src/App/Utilities/SendIconGlyphConverter.cs @@ -3,7 +3,8 @@ using System.Globalization; using Bit.Core; using Bit.Core.Enums; using Bit.Core.Models.View; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/StringHasValueConverter.cs b/src/App/Utilities/StringHasValueConverter.cs index 4410c9143..b0f3245db 100644 --- a/src/App/Utilities/StringHasValueConverter.cs +++ b/src/App/Utilities/StringHasValueConverter.cs @@ -1,5 +1,6 @@ using System; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/ThemeManager.cs b/src/App/Utilities/ThemeManager.cs index 65d2d2cfe..9d98db4aa 100644 --- a/src/App/Utilities/ThemeManager.cs +++ b/src/App/Utilities/ThemeManager.cs @@ -6,7 +6,9 @@ using Bit.App.Styles; using Bit.Core.Abstractions; using Bit.Core.Services; using Bit.Core.Utilities; -using Xamarin.Forms; +using Microsoft.Maui.ApplicationModel; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { @@ -60,7 +62,8 @@ namespace Bit.App.Utilities resources.MergedDictionaries.Add(new Base()); // Platform styles - if (Device.RuntimePlatform == Device.Android) + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + if (Device.RuntimePlatform == Device.Android) { resources.MergedDictionaries.Add(new Android()); } @@ -147,9 +150,9 @@ namespace Bit.App.Utilities { // called from iOS extension var app = new App(new AppOptions { IosExtension = true }); - return app.RequestedTheme == OSAppTheme.Dark; + return app.RequestedTheme == AppTheme.Dark; } - return Application.Current.RequestedTheme == OSAppTheme.Dark; + return Application.Current.RequestedTheme == AppTheme.Dark; } public static void ApplyResourcesTo(VisualElement element) diff --git a/src/App/Utilities/TimerTask.cs b/src/App/Utilities/TimerTask.cs index 0288408ac..27e0b81af 100644 --- a/src/App/Utilities/TimerTask.cs +++ b/src/App/Utilities/TimerTask.cs @@ -2,7 +2,8 @@ using System.Threading; using System.Threading.Tasks; using Bit.Core.Abstractions; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/UpperCaseConverter.cs b/src/App/Utilities/UpperCaseConverter.cs index 592f95326..a02c83de9 100644 --- a/src/App/Utilities/UpperCaseConverter.cs +++ b/src/App/Utilities/UpperCaseConverter.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/App/Utilities/VerificationActionsFlowHelper.cs b/src/App/Utilities/VerificationActionsFlowHelper.cs index eaf3376e9..9955233fc 100644 --- a/src/App/Utilities/VerificationActionsFlowHelper.cs +++ b/src/App/Utilities/VerificationActionsFlowHelper.cs @@ -7,7 +7,8 @@ using Bit.App.Pages.Accounts; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Utilities; -using Xamarin.Forms; +using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Bit.App.Utilities { diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 76c88095e..2ed069187 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -1,17 +1,14 @@  - - netstandard2.1 + net7.0 Bit.Core BitwardenCore Debug;Release;FDroid - pdbonly true - @@ -22,17 +19,13 @@ - - - - @@ -40,11 +33,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + - - + \ No newline at end of file diff --git a/src/Maui/Bitwarden/Abstractions/IAccountsManager.cs b/src/Maui/Bitwarden/Abstractions/IAccountsManager.cs new file mode 100644 index 000000000..ab78c6ca4 --- /dev/null +++ b/src/Maui/Bitwarden/Abstractions/IAccountsManager.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading.Tasks; +using Bit.App.Models; + +namespace Bit.App.Abstractions +{ + public interface IAccountsManager + { + void Init(Func getOptionsFunc, IAccountsManagerHost accountsManagerHost); + Task NavigateOnAccountChangeAsync(bool? isAuthed = null); + Task StartDefaultNavigationFlowAsync(Action appOptionsAction); + Task LogOutAsync(string userId, bool userInitiated, bool expired); + Task PromptToSwitchToExistingAccountAsync(string userId); + } +} diff --git a/src/Maui/Bitwarden/Abstractions/IAccountsManagerHost.cs b/src/Maui/Bitwarden/Abstractions/IAccountsManagerHost.cs new file mode 100644 index 000000000..17e5ae0e2 --- /dev/null +++ b/src/Maui/Bitwarden/Abstractions/IAccountsManagerHost.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Bit.Core.Enums; + +namespace Bit.App.Abstractions +{ + public interface INavigationParams { } + + public interface IAccountsManagerHost + { + Task SetPreviousPageInfoAsync(); + void Navigate(NavigationTarget navTarget, INavigationParams navParams = null); + Task UpdateThemeAsync(); + } +} diff --git a/src/Maui/Bitwarden/Abstractions/IDeepLinkContext.cs b/src/Maui/Bitwarden/Abstractions/IDeepLinkContext.cs new file mode 100644 index 000000000..345596d50 --- /dev/null +++ b/src/Maui/Bitwarden/Abstractions/IDeepLinkContext.cs @@ -0,0 +1,9 @@ +using System; + +namespace Bit.App.Abstractions +{ + public interface IDeepLinkContext + { + bool OnNewUri(Uri uri); + } +} diff --git a/src/Maui/Bitwarden/Abstractions/IDeviceActionService.cs b/src/Maui/Bitwarden/Abstractions/IDeviceActionService.cs new file mode 100644 index 000000000..c8b7e94f8 --- /dev/null +++ b/src/Maui/Bitwarden/Abstractions/IDeviceActionService.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using Bit.App.Utilities.Prompts; +using Bit.Core.Enums; +using Bit.Core.Models; + +namespace Bit.App.Abstractions +{ + public interface IDeviceActionService + { + string DeviceUserAgent { get; } + Bit.Core.Enums.DeviceType DeviceType { get; } + int SystemMajorVersion(); + string SystemModel(); + string GetBuildNumber(); + + void Toast(string text, bool longDuration = false); + Task ShowLoadingAsync(string text); + Task HideLoadingAsync(); + Task DisplayPromptAync(string title = null, string description = null, string text = null, + string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false, + bool autofocus = true, bool password = false); + Task DisplayValidatablePromptAsync(ValidatablePromptConfig config); + Task DisplayAlertAsync(string title, string message, string cancel, params string[] buttons); + Task DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons); + + bool SupportsFaceBiometric(); + Task SupportsFaceBiometricAsync(); + bool SupportsNfc(); + bool SupportsCamera(); + bool SupportsFido2(); + + bool LaunchApp(string appName); + void RateApp(); + void OpenAccessibilitySettings(); + void OpenAccessibilityOverlayPermissionSettings(); + void OpenAutofillSettings(); + long GetActiveTime(); + void CloseMainApp(); + float GetSystemFontSizeScale(); + Task OnAccountSwitchCompleteAsync(); + Task SetScreenCaptureAllowedAsync(); + void OpenAppSettings(); + void CloseExtensionPopUp(); + } +} diff --git a/src/Maui/Bitwarden/Abstractions/ILocalizeService.cs b/src/Maui/Bitwarden/Abstractions/ILocalizeService.cs new file mode 100644 index 000000000..09b0fb924 --- /dev/null +++ b/src/Maui/Bitwarden/Abstractions/ILocalizeService.cs @@ -0,0 +1,22 @@ +using System; +using System.Globalization; + +namespace Bit.App.Abstractions +{ + public interface ILocalizeService + { + CultureInfo GetCurrentCultureInfo(); + + /// + /// Format date using device locale. + /// Needed for iOS as it provides locales unsupported in .Net + /// + string GetLocaleShortDate(DateTime? date); + + /// + /// Format time using device locale. + /// Needed for iOS as it provides locales unsupported in .Net + /// + string GetLocaleShortTime(DateTime? time); + } +} diff --git a/src/Maui/Bitwarden/Abstractions/IPasswordRepromptService.cs b/src/Maui/Bitwarden/Abstractions/IPasswordRepromptService.cs new file mode 100644 index 000000000..579d9ab44 --- /dev/null +++ b/src/Maui/Bitwarden/Abstractions/IPasswordRepromptService.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; + +namespace Bit.App.Abstractions +{ + public interface IPasswordRepromptService + { + string[] ProtectedFields { get; } + + Task ShowPasswordPromptAsync(); + + Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync(); + + Task Enabled(); + } +} diff --git a/src/Maui/Bitwarden/Abstractions/IPushNotificationListenerService.cs b/src/Maui/Bitwarden/Abstractions/IPushNotificationListenerService.cs new file mode 100644 index 000000000..fdbb6ca88 --- /dev/null +++ b/src/Maui/Bitwarden/Abstractions/IPushNotificationListenerService.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using Bit.App.Models; +using Newtonsoft.Json.Linq; + +namespace Bit.App.Abstractions +{ + public interface IPushNotificationListenerService + { + Task OnMessageAsync(JObject values, string device); + Task OnRegisteredAsync(string token, string device); + void OnUnregistered(string device); + void OnError(string message, string device); + Task OnNotificationTapped(BaseNotificationData data); + Task OnNotificationDismissed(BaseNotificationData data); + bool ShouldShowNotification(); + } +} diff --git a/src/Maui/Bitwarden/Abstractions/IPushNotificationService.cs b/src/Maui/Bitwarden/Abstractions/IPushNotificationService.cs new file mode 100644 index 000000000..5d69be1aa --- /dev/null +++ b/src/Maui/Bitwarden/Abstractions/IPushNotificationService.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.App.Models; + +namespace Bit.App.Abstractions +{ + public interface IPushNotificationService + { + bool IsRegisteredForPush { get; } + Task AreNotificationsSettingsEnabledAsync(); + Task GetTokenAsync(); + Task RegisterAsync(); + Task UnregisterAsync(); + void SendLocalNotification(string title, string message, BaseNotificationData data); + void DismissLocalNotification(string notificationId); + } +} diff --git a/src/Maui/Bitwarden/App.xaml b/src/Maui/Bitwarden/App.xaml new file mode 100644 index 000000000..58b8d50fc --- /dev/null +++ b/src/Maui/Bitwarden/App.xaml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/src/Maui/Bitwarden/App.xaml.cs b/src/Maui/Bitwarden/App.xaml.cs new file mode 100644 index 000000000..7df514569 --- /dev/null +++ b/src/Maui/Bitwarden/App.xaml.cs @@ -0,0 +1,546 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Models; +using Bit.App.Pages; +using Bit.App.Resources; +using Bit.App.Services; +using Bit.App.Utilities; +using Bit.App.Utilities.AccountManagement; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Response; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Microsoft.Maui.Controls.Xaml; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +[assembly: XamlCompilation(XamlCompilationOptions.Compile)] +namespace Bit.App +{ + public partial class App : Application, IAccountsManagerHost + { + public const string POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE = "popAllAndGoToTabGenerator"; + public const string POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE = "popAllAndGoToTabMyVault"; + public const string POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE = "popAllAndGoToTabSend"; + public const string POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE = "popAllAndGoToAutofillCiphers"; + + private readonly IBroadcasterService _broadcasterService; + private readonly IMessagingService _messagingService; + private readonly IStateService _stateService; + private readonly IVaultTimeoutService _vaultTimeoutService; + private readonly ISyncService _syncService; + private readonly IAuthService _authService; + private readonly IDeviceActionService _deviceActionService; + private readonly IFileService _fileService; + private readonly IAccountsManager _accountsManager; + private readonly IPushNotificationService _pushNotificationService; + private readonly IConfigService _configService; + private static bool _isResumed; + // these variables are static because the app is launching new activities on notification click, creating new instances of App. + private static bool _pendingCheckPasswordlessLoginRequests; + private static object _processingLoginRequestLock = new object(); + + public App(AppOptions appOptions) + { + Options = appOptions ?? new AppOptions(); + if (Options.IosExtension) + { + Current = this; + return; + } + _broadcasterService = ServiceContainer.Resolve("broadcasterService"); + _messagingService = ServiceContainer.Resolve("messagingService"); + _stateService = ServiceContainer.Resolve("stateService"); + _vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); + _syncService = ServiceContainer.Resolve("syncService"); + _authService = ServiceContainer.Resolve("authService"); + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _fileService = ServiceContainer.Resolve(); + _accountsManager = ServiceContainer.Resolve("accountsManager"); + _pushNotificationService = ServiceContainer.Resolve(); + _configService = ServiceContainer.Resolve(); + + _accountsManager.Init(() => Options, this); + + Bootstrap(); + _broadcasterService.Subscribe(nameof(App), async (message) => + { + try + { + if (message.Command == "showDialog") + { + var details = message.Data as DialogDetails; + var confirmed = true; + var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ? + AppResources.Ok : details.ConfirmText; + Device.BeginInvokeOnMainThread(async () => + { + if (!string.IsNullOrWhiteSpace(details.CancelText)) + { + confirmed = await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText, + details.CancelText); + } + else + { + await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText); + } + _messagingService.Send("showDialogResolve", new Tuple(details.DialogId, confirmed)); + }); + } + else if (message.Command == "resumed") + { + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + if (Device.RuntimePlatform == Device.iOS) + { + ResumedAsync().FireAndForget(); + } + } + else if (message.Command == "slept") + { + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + if (Device.RuntimePlatform == Device.iOS) + { + await SleptAsync(); + } + } + else if (message.Command == "migrated") + { + await Task.Delay(1000); + await _accountsManager.NavigateOnAccountChangeAsync(); + } + else if (message.Command == POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE || + message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE || + message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE || + message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE || + message.Command == DeepLinkContext.NEW_OTP_MESSAGE) + { + if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE) + { + Options.OtpData = new OtpData((string)message.Data); + } + + Device.InvokeOnMainThreadAsync(async () => + { + if (Current.MainPage is TabsPage tabsPage) + { + while (tabsPage.Navigation.ModalStack.Count > 0) + { + await tabsPage.Navigation.PopModalAsync(false); + } + if (message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE) + { + Current.MainPage = new NavigationPage(new CipherSelectionPage(Options)); + } + else if (message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE) + { + Options.MyVaultTile = false; + tabsPage.ResetToVaultPage(); + } + else if (message.Command == POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE) + { + Options.GeneratorTile = false; + tabsPage.ResetToGeneratorPage(); + } + else if (message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE) + { + tabsPage.ResetToSendPage(); + } + else if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE) + { + tabsPage.ResetToVaultPage(); + await tabsPage.Navigation.PushModalAsync(new NavigationPage(new CipherSelectionPage(Options))); + } + } + }); + } + else if (message.Command == "convertAccountToKeyConnector") + { + Device.BeginInvokeOnMainThread(async () => + { + await Application.Current.MainPage.Navigation.PushModalAsync( + new NavigationPage(new RemoveMasterPasswordPage())); + }); + } + else if (message.Command == Constants.ForceUpdatePassword) + { + Device.BeginInvokeOnMainThread(async () => + { + await Application.Current.MainPage.Navigation.PushModalAsync( + new NavigationPage(new UpdateTempPasswordPage())); + }); + } + else if (message.Command == "syncCompleted") + { + await _configService.GetAsync(true); + } + else if (message.Command == Constants.PasswordlessLoginRequestKey + || message.Command == "unlocked" + || message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED) + { + lock (_processingLoginRequestLock) + { + // lock doesn't allow for async execution + CheckPasswordlessLoginRequestsAsync().Wait(); + } + } + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } + }); + } + + private async Task CheckPasswordlessLoginRequestsAsync() + { + if (!_isResumed) + { + _pendingCheckPasswordlessLoginRequests = true; + return; + } + _pendingCheckPasswordlessLoginRequests = false; + if (await _vaultTimeoutService.IsLockedAsync()) + { + return; + } + + var notification = await _stateService.GetPasswordlessLoginNotificationAsync(); + if (notification == null) + { + return; + } + + if (await CheckShouldSwitchActiveUserAsync(notification)) + { + return; + } + + // Delay to wait for the vault page to appear + await Task.Delay(2000); + // if there is a request modal opened ignore all incoming requests + if (App.Current.MainPage.Navigation.ModalStack.Any(p => p is NavigationPage navPage && navPage.CurrentPage is LoginPasswordlessPage)) + { + return; + } + var loginRequestData = await _authService.GetPasswordlessLoginRequestByIdAsync(notification.Id); + var page = new LoginPasswordlessPage(new LoginPasswordlessDetails() + { + PubKey = loginRequestData.PublicKey, + Id = loginRequestData.Id, + IpAddress = loginRequestData.RequestIpAddress, + Email = await _stateService.GetEmailAsync(), + FingerprintPhrase = loginRequestData.FingerprintPhrase, + RequestDate = loginRequestData.CreationDate, + DeviceType = loginRequestData.RequestDeviceType, + Origin = loginRequestData.Origin + }); + await _stateService.SetPasswordlessLoginNotificationAsync(null); + _pushNotificationService.DismissLocalNotification(Constants.PasswordlessNotificationId); + if (!loginRequestData.IsExpired) + { + await Device.InvokeOnMainThreadAsync(() => Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page))); + } + } + + private async Task CheckShouldSwitchActiveUserAsync(PasswordlessRequestNotification notification) + { + var activeUserId = await _stateService.GetActiveUserIdAsync(); + if (notification.UserId == activeUserId) + { + return false; + } + + var notificationUserEmail = await _stateService.GetEmailAsync(notification.UserId); + Device.BeginInvokeOnMainThread(async () => + { + try + { + var result = await _deviceActionService.DisplayAlertAsync(AppResources.LogInRequested, string.Format(AppResources.LoginAttemptFromXDoYouWantToSwitchToThisAccount, notificationUserEmail), AppResources.Cancel, AppResources.Ok); + if (result == AppResources.Ok) + { + await _stateService.SetActiveUserAsync(notification.UserId); + _messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT); + } + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } + }); + return true; + } + + public AppOptions Options { get; private set; } + + protected async override void OnStart() + { + System.Diagnostics.Debug.WriteLine("XF App: OnStart"); + _isResumed = true; + await ClearCacheIfNeededAsync(); + Prime(); + if (string.IsNullOrWhiteSpace(Options.Uri)) + { + var updated = await AppHelpers.PerformUpdateTasksAsync(_syncService, _deviceActionService, + _stateService); + if (!updated) + { + SyncIfNeeded(); + } + } + if (_pendingCheckPasswordlessLoginRequests) + { + _messagingService.Send(Constants.PasswordlessLoginRequestKey); + } + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + if (Device.RuntimePlatform == Device.Android) + { + await _vaultTimeoutService.CheckVaultTimeoutAsync(); + // Reset delay on every start + _vaultTimeoutService.DelayLockAndLogoutMs = null; + } + + await _configService.GetAsync(); + _messagingService.Send("startEventTimer"); + } + + protected async override void OnSleep() + { + System.Diagnostics.Debug.WriteLine("XF App: OnSleep"); + _isResumed = false; + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + if (Device.RuntimePlatform == Device.Android) + { + var isLocked = await _vaultTimeoutService.IsLockedAsync(); + if (!isLocked) + { + await _stateService.SetLastActiveTimeAsync(_deviceActionService.GetActiveTime()); + } + if (!SetTabsPageFromAutofill(isLocked)) + { + ClearAutofillUri(); + } + await SleptAsync(); + } + } + + protected override void OnResume() + { + System.Diagnostics.Debug.WriteLine("XF App: OnResume"); + _isResumed = true; + if (_pendingCheckPasswordlessLoginRequests) + { + _messagingService.Send(Constants.PasswordlessLoginRequestKey); + } + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + if (Device.RuntimePlatform == Device.Android) + { + ResumedAsync().FireAndForget(); + } + } + + private async Task SleptAsync() + { + await _vaultTimeoutService.CheckVaultTimeoutAsync(); + await ClearSensitiveFieldsAsync(); + _messagingService.Send("stopEventTimer"); + } + + private async Task ResumedAsync() + { + await _stateService.CheckExtensionActiveUserAndSwitchIfNeededAsync(); + await _vaultTimeoutService.CheckVaultTimeoutAsync(); + await ClearSensitiveFieldsAsync(); + _messagingService.Send("startEventTimer"); + await UpdateThemeAsync(); + await ClearCacheIfNeededAsync(); + Prime(); + SyncIfNeeded(); + if (Current.MainPage is NavigationPage navPage && navPage.CurrentPage is LockPage lockPage) + { + await lockPage.PromptBiometricAfterResumeAsync(); + } + } + + public async Task UpdateThemeAsync() + { + await Device.InvokeOnMainThreadAsync(() => + { + ThemeManager.SetTheme(Current.Resources); + _messagingService.Send("updatedTheme"); + }); + } + + private async Task ClearSensitiveFieldsAsync() + { + await Device.InvokeOnMainThreadAsync(() => + { + _messagingService.Send(Constants.ClearSensitiveFields); + }); + } + + private void SetCulture() + { + // Calendars are removed by linker. ref https://bugzilla.xamarin.com/show_bug.cgi?id=59077 + new System.Globalization.ThaiBuddhistCalendar(); + new System.Globalization.HijriCalendar(); + new System.Globalization.UmAlQuraCalendar(); + } + + private async Task ClearCacheIfNeededAsync() + { + var lastClear = await _stateService.GetLastFileCacheClearAsync(); + if ((DateTime.UtcNow - lastClear.GetValueOrDefault(DateTime.MinValue)).TotalDays >= 1) + { + var task = Task.Run(() => _fileService.ClearCacheAsync()); + } + } + + private void ClearAutofillUri() + { + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + if (Device.RuntimePlatform == Device.Android && !string.IsNullOrWhiteSpace(Options.Uri)) + { + Options.Uri = null; + } + } + + private bool SetTabsPageFromAutofill(bool isLocked) + { + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + if (Device.RuntimePlatform == Device.Android && !string.IsNullOrWhiteSpace(Options.Uri) && + !Options.FromAutofillFramework) + { + Task.Run(() => + { + Device.BeginInvokeOnMainThread(() => + { + Options.Uri = null; + if (isLocked) + { + Current.MainPage = new NavigationPage(new LockPage()); + } + else + { + Current.MainPage = new TabsPage(); + } + }); + }); + return true; + } + return false; + } + + private void Prime() + { + Task.Run(() => + { + var word = EEFLongWordList.Instance.List[1]; + var parsedDomain = DomainName.TryParse("https://bitwarden.com", out var domainName); + }); + } + + private void Bootstrap() + { + InitializeComponent(); + SetCulture(); + ThemeManager.SetTheme(Current.Resources); + Current.RequestedThemeChanged += (s, a) => + { + UpdateThemeAsync(); + }; + Current.MainPage = new NavigationPage(new HomePage(Options)); + _accountsManager.NavigateOnAccountChangeAsync().FireAndForget(); + ServiceContainer.Resolve("platformUtilsService").Init(); + } + + private void SyncIfNeeded() + { + if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None) + { + return; + } + Task.Run(async () => + { + var lastSync = await _syncService.GetLastSyncAsync(); + if (lastSync == null || ((DateTime.UtcNow - lastSync) > TimeSpan.FromMinutes(30))) + { + await Task.Delay(1000); + await _syncService.FullSyncAsync(false); + } + }); + } + + public async Task SetPreviousPageInfoAsync() + { + PreviousPageInfo lastPageBeforeLock = null; + if (Current.MainPage is TabbedPage tabbedPage && tabbedPage.Navigation.ModalStack.Count > 0) + { + var topPage = tabbedPage.Navigation.ModalStack[tabbedPage.Navigation.ModalStack.Count - 1]; + if (topPage is NavigationPage navPage) + { + if (navPage.CurrentPage is CipherDetailsPage cipherDetailsPage) + { + lastPageBeforeLock = new PreviousPageInfo + { + Page = "view", + CipherId = cipherDetailsPage.ViewModel.CipherId + }; + } + else if (navPage.CurrentPage is CipherAddEditPage cipherAddEditPage && cipherAddEditPage.ViewModel.EditMode) + { + lastPageBeforeLock = new PreviousPageInfo + { + Page = "edit", + CipherId = cipherAddEditPage.ViewModel.CipherId + }; + } + } + } + await _stateService.SetPreviousPageInfoAsync(lastPageBeforeLock); + } + + public void Navigate(NavigationTarget navTarget, INavigationParams navParams) + { + switch (navTarget) + { + case NavigationTarget.HomeLogin: + Current.MainPage = new NavigationPage(new HomePage(Options)); + break; + case NavigationTarget.Login: + if (navParams is LoginNavigationParams loginParams) + { + Current.MainPage = new NavigationPage(new LoginPage(loginParams.Email, Options)); + } + break; + case NavigationTarget.Lock: + if (navParams is LockNavigationParams lockParams) + { + Current.MainPage = new NavigationPage(new LockPage(Options, lockParams.AutoPromptBiometric)); + } + else + { + Current.MainPage = new NavigationPage(new LockPage(Options)); + } + break; + case NavigationTarget.Home: + Current.MainPage = new TabsPage(Options); + break; + case NavigationTarget.AddEditCipher: + Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: Options)); + break; + case NavigationTarget.AutofillCiphers: + case NavigationTarget.OtpCipherSelection: + Current.MainPage = new NavigationPage(new CipherSelectionPage(Options)); + break; + case NavigationTarget.SendAddEdit: + Current.MainPage = new NavigationPage(new SendAddEditPage(Options)); + break; + } + } + } +} diff --git a/src/Maui/Bitwarden/Behaviors/EditorPreventAutoBottomScrollingOnFocusedBehavior.cs b/src/Maui/Bitwarden/Behaviors/EditorPreventAutoBottomScrollingOnFocusedBehavior.cs new file mode 100644 index 000000000..618f821ce --- /dev/null +++ b/src/Maui/Bitwarden/Behaviors/EditorPreventAutoBottomScrollingOnFocusedBehavior.cs @@ -0,0 +1,43 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Behaviors +{ + /// + /// This behavior prevents the Editor to be automatically scrolled to the bottom on focus. + /// This is needed due to this Xamarin Forms issue: https://github.com/xamarin/Xamarin.Forms/issues/2233 + /// + public class EditorPreventAutoBottomScrollingOnFocusedBehavior : Behavior + { + public static readonly BindableProperty ParentScrollViewProperty + = BindableProperty.Create(nameof(ParentScrollView), typeof(ScrollView), typeof(EditorPreventAutoBottomScrollingOnFocusedBehavior)); + + public ScrollView ParentScrollView + { + get => (ScrollView)GetValue(ParentScrollViewProperty); + set => SetValue(ParentScrollViewProperty, value); + } + + protected override void OnAttachedTo(Editor bindable) + { + base.OnAttachedTo(bindable); + + bindable.Focused += OnFocused; + } + + private void OnFocused(object sender, FocusEventArgs e) + { + if (DeviceInfo.Platform.Equals(DevicePlatform.iOS) && ParentScrollView != null) + { + ParentScrollView.ScrollToAsync(ParentScrollView.ScrollX, ParentScrollView.ScrollY, true); + } + } + + protected override void OnDetachingFrom(Editor bindable) + { + bindable.Focused -= OnFocused; + + base.OnDetachingFrom(bindable); + } + } +} diff --git a/src/Maui/Bitwarden/Bitwarden.csproj b/src/Maui/Bitwarden/Bitwarden.csproj new file mode 100644 index 000000000..56bcb15f3 --- /dev/null +++ b/src/Maui/Bitwarden/Bitwarden.csproj @@ -0,0 +1,357 @@ + + + + net7.0-android;net7.0-ios + $(TargetFrameworks);net7.0-windows10.0.19041.0 + + + Exe + Bit.App + true + true + enable + + + Bitwarden + + + com.x8bit.bitwarden + ccf4766c-a36c-4647-900c-0ea7d323ccc6 + + + 1.0 + 1 + + 11.0 + 13.1 + 21.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Maui/Bitwarden/Bitwarden.sln b/src/Maui/Bitwarden/Bitwarden.sln new file mode 100644 index 000000000..b87cddfac --- /dev/null +++ b/src/Maui/Bitwarden/Bitwarden.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 25.0.1706.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden", "Bitwarden.csproj", "{58FBB5C6-CBD6-470D-90A6-00E89C0CD817}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + FDroid|Any CPU = FDroid|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {58FBB5C6-CBD6-470D-90A6-00E89C0CD817}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58FBB5C6-CBD6-470D-90A6-00E89C0CD817}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58FBB5C6-CBD6-470D-90A6-00E89C0CD817}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58FBB5C6-CBD6-470D-90A6-00E89C0CD817}.Release|Any CPU.Build.0 = Release|Any CPU + {58FBB5C6-CBD6-470D-90A6-00E89C0CD817}.FDroid|Any CPU.ActiveCfg = Debug|Any CPU + {58FBB5C6-CBD6-470D-90A6-00E89C0CD817}.FDroid|Any CPU.Build.0 = Debug|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F599106B-483D-46B0-BB58-A6F3AE484CE3} + EndGlobalSection +EndGlobal diff --git a/src/Maui/Bitwarden/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml b/src/Maui/Bitwarden/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml new file mode 100644 index 000000000..1f6580e3f --- /dev/null +++ b/src/Maui/Bitwarden/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Maui/Bitwarden/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs b/src/Maui/Bitwarden/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs new file mode 100644 index 000000000..3db4084f1 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs @@ -0,0 +1,196 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Input; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using Bit.App.Utilities; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public partial class AccountSwitchingOverlayView : ContentView + { + public static readonly BindableProperty MainPageProperty = BindableProperty.Create( + nameof(MainPage), + typeof(ContentPage), + typeof(AccountSwitchingOverlayView), + defaultBindingMode: BindingMode.OneWay); + + public static readonly BindableProperty MainFabProperty = BindableProperty.Create( + nameof(MainFab), + typeof(View), + typeof(AccountSwitchingOverlayView), + defaultBindingMode: BindingMode.OneWay); + + public ContentPage MainPage + { + get => (ContentPage)GetValue(MainPageProperty); + set => SetValue(MainPageProperty, value); + } + + public View MainFab + { + get => (View)GetValue(MainFabProperty); + set => SetValue(MainFabProperty, value); + } + + readonly LazyResolve _logger = new LazyResolve("logger"); + + public AccountSwitchingOverlayView() + { + InitializeComponent(); + + ToggleVisibililtyCommand = new AsyncCommand(ToggleVisibilityAsync, + onException: ex => _logger.Value.Exception(ex), + allowsMultipleExecutions: false); + + SelectAccountCommand = new AsyncCommand(SelectAccountAsync, + onException: ex => _logger.Value.Exception(ex), + allowsMultipleExecutions: false); + + LongPressAccountCommand = new AsyncCommand(LongPressAccountAsync, + onException: ex => _logger.Value.Exception(ex), + allowsMultipleExecutions: false); + } + + public AccountSwitchingOverlayViewModel ViewModel => BindingContext as AccountSwitchingOverlayViewModel; + + public ICommand ToggleVisibililtyCommand { get; } + + public ICommand SelectAccountCommand { get; } + + public ICommand LongPressAccountCommand { get; } + + public int AccountListRowHeight => // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes +Device.RuntimePlatform == Device.Android ? 74 : 70; + + public bool LongPressAccountEnabled { get; set; } = true; + + public Action AfterHide { get; set; } + + public async Task ToggleVisibilityAsync() + { + if (IsVisible) + { + await HideAsync(); + } + else + { + await ShowAsync(); + } + } + + public async Task ShowAsync() + { + if (ViewModel == null) + { + return; + } + + await ViewModel.RefreshAccountViewsAsync(); + + await Device.InvokeOnMainThreadAsync(async () => + { + // start listView in default (off-screen) position + await _accountListContainer.TranslateTo(0, _accountListContainer.Height * -1, 0); + + // re-measure in case accounts have been removed without changing screens + if (ViewModel.AccountViews != null) + { + _accountListView.HeightRequest = AccountListRowHeight * ViewModel.AccountViews.Count; + } + + // set overlay opacity to zero before making visible and start fade-in + Opacity = 0; + IsVisible = true; + this.FadeTo(1, 100); + + if (Device.RuntimePlatform == Device.Android && MainFab != null) + { + // start fab fade-out + MainFab.FadeTo(0, 200); + } + + // slide account list into view + await _accountListContainer.TranslateTo(0, 0, 200, Easing.SinOut); + }); + } + + public async Task HideAsync() + { + if (!IsVisible) + { + // already hidden, don't animate again + return; + } + // Not all animations are awaited. This is intentional to allow multiple simultaneous animations. + await Device.InvokeOnMainThreadAsync(async () => + { + // start overlay fade-out + this.FadeTo(0, 200); + + if (Device.RuntimePlatform == Device.Android && MainFab != null) + { + // start fab fade-in + MainFab.FadeTo(1, 200); + } + + // slide account list out of view + await _accountListContainer.TranslateTo(0, _accountListContainer.Height * -1, 200, Easing.SinIn); + + // remove overlay + IsVisible = false; + + AfterHide?.Invoke(); + }); + } + + private async void FreeSpaceOverlay_Tapped(object sender, EventArgs e) + { + try + { + await HideAsync(); + } + catch (Exception ex) + { + _logger.Value.Exception(ex); + } + } + + private async Task SelectAccountAsync(AccountViewCellViewModel item) + { + try + { + await Task.Delay(100); + await HideAsync(); + + ViewModel?.SelectAccountCommand?.Execute(item); + } + catch (Exception ex) + { + _logger.Value.Exception(ex); + } + } + + private async Task LongPressAccountAsync(AccountViewCellViewModel item) + { + if (!LongPressAccountEnabled || !item.IsAccount) + { + return; + } + try + { + await Task.Delay(100); + await HideAsync(); + + ViewModel?.LongPressAccountCommand?.Execute( + new Tuple(MainPage, item)); + } + catch (Exception ex) + { + _logger.Value.Exception(ex); + } + } + } +} diff --git a/src/Maui/Bitwarden/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs b/src/Maui/Bitwarden/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs new file mode 100644 index 000000000..272201818 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Windows.Input; +using Bit.App.Utilities; +using Bit.Core.Abstractions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class AccountSwitchingOverlayViewModel : ExtendedViewModel + { + private readonly IStateService _stateService; + private readonly IMessagingService _messagingService; + + public AccountSwitchingOverlayViewModel(IStateService stateService, + IMessagingService messagingService, + ILogger logger) + { + _stateService = stateService; + _messagingService = messagingService; + + SelectAccountCommand = new AsyncCommand(SelectAccountAsync, + onException: ex => logger.Exception(ex), + allowsMultipleExecutions: false); + + LongPressAccountCommand = new AsyncCommand>(LongPressAccountAsync, + onException: ex => logger.Exception(ex), + allowsMultipleExecutions: false); + } + + // this needs to be a new list every time for the binding to get updated, + // XF doesn't currentlyl provide a direct way to update on same instance + // https://github.com/xamarin/Xamarin.Forms/issues/1950 + public List AccountViews => _stateService?.AccountViews is null ? null : new List(_stateService.AccountViews); + + public bool AllowActiveAccountSelection { get; set; } + + public bool AllowAddAccountRow { get; set; } + + public ICommand SelectAccountCommand { get; } + + public ICommand LongPressAccountCommand { get; } + + public bool FromIOSExtension { get; set; } + + private async Task SelectAccountAsync(AccountViewCellViewModel item) + { + if (!item.AccountView.IsAccount) + { + _messagingService.Send(AccountsManagerMessageCommands.ADD_ACCOUNT); + return; + } + + if (!item.AccountView.IsActive) + { + await _stateService.SetActiveUserAsync(item.AccountView.UserId); + _messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT); + if (FromIOSExtension) + { + await _stateService.SaveExtensionActiveUserIdToStorageAsync(item.AccountView.UserId); + } + } + else if (AllowActiveAccountSelection) + { + _messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT); + } + } + + private async Task LongPressAccountAsync(Tuple item) + { + var (page, account) = item; + if (account.AccountView.IsAccount) + { + await AppHelpers.AccountListOptions(page, account); + } + } + + public async Task RefreshAccountViewsAsync() + { + await _stateService.RefreshAccountViewsAsync(AllowAddAccountRow); + + Device.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(AccountViews))); + } + } +} diff --git a/src/Maui/Bitwarden/Controls/AccountViewCell/AccountViewCell.xaml b/src/Maui/Bitwarden/Controls/AccountViewCell/AccountViewCell.xaml new file mode 100644 index 000000000..9d270b67a --- /dev/null +++ b/src/Maui/Bitwarden/Controls/AccountViewCell/AccountViewCell.xaml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Maui/Bitwarden/Controls/AccountViewCell/AccountViewCell.xaml.cs b/src/Maui/Bitwarden/Controls/AccountViewCell/AccountViewCell.xaml.cs new file mode 100644 index 000000000..65c1b9008 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/AccountViewCell/AccountViewCell.xaml.cs @@ -0,0 +1,55 @@ +using System.Windows.Input; +using Bit.Core.Models.View; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public partial class AccountViewCell : ViewCell + { + public static readonly BindableProperty AccountProperty = BindableProperty.Create( + nameof(Account), typeof(AccountView), typeof(AccountViewCell)); + + public static readonly BindableProperty SelectAccountCommandProperty = BindableProperty.Create( + nameof(SelectAccountCommand), typeof(ICommand), typeof(AccountViewCell)); + + public static readonly BindableProperty LongPressAccountCommandProperty = BindableProperty.Create( + nameof(LongPressAccountCommand), typeof(ICommand), typeof(AccountViewCell)); + + public AccountViewCell() + { + InitializeComponent(); + } + + public AccountView Account + { + get => GetValue(AccountProperty) as AccountView; + set => SetValue(AccountProperty, value); + } + + public ICommand SelectAccountCommand + { + get => GetValue(SelectAccountCommandProperty) as ICommand; + set => SetValue(SelectAccountCommandProperty, value); + } + + public ICommand LongPressAccountCommand + { + get => GetValue(LongPressAccountCommandProperty) as ICommand; + set => SetValue(LongPressAccountCommandProperty, value); + } + + protected override void OnPropertyChanged(string propertyName = null) + { + base.OnPropertyChanged(propertyName); + if (propertyName == AccountProperty.PropertyName) + { + if (Account == null) + { + return; + } + BindingContext = new AccountViewCellViewModel(Account); + } + } + } +} diff --git a/src/Maui/Bitwarden/Controls/AccountViewCell/AccountViewCellViewModel.cs b/src/Maui/Bitwarden/Controls/AccountViewCell/AccountViewCellViewModel.cs new file mode 100644 index 000000000..45e2f2455 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/AccountViewCell/AccountViewCellViewModel.cs @@ -0,0 +1,94 @@ +using Bit.Core; +using Bit.Core.Enums; +using Bit.Core.Models.View; +using Bit.Core.Utilities; + +namespace Bit.App.Controls +{ + public class AccountViewCellViewModel : ExtendedViewModel + { + private AccountView _accountView; + private AvatarImageSource _avatar; + + public AccountViewCellViewModel(AccountView accountView) + { + AccountView = accountView; + AvatarImageSource = ServiceContainer.Resolve("avatarImageSourcePool") + ?.GetOrCreateAvatar(AccountView.UserId, AccountView.Name, AccountView.Email, AccountView.AvatarColor); + } + + public AccountView AccountView + { + get => _accountView; + set => SetProperty(ref _accountView, value); + } + + public AvatarImageSource AvatarImageSource + { + get => _avatar; + set => SetProperty(ref _avatar, value); + } + + public bool IsAccount + { + get => AccountView.IsAccount; + } + + public bool ShowHostname + { + get => !string.IsNullOrWhiteSpace(AccountView.Hostname) && AccountView.Hostname != "vault.bitwarden.com"; + } + + public bool IsActive + { + get => AccountView.IsActive; + } + + public bool IsUnlocked + { + get => AccountView.AuthStatus == AuthenticationStatus.Unlocked; + } + + public bool IsUnlockedAndNotActive + { + get => IsUnlocked && !IsActive; + } + + public bool IsLocked + { + get => AccountView.AuthStatus == AuthenticationStatus.Locked; + } + + public bool IsLockedAndNotActive + { + get => IsLocked && !IsActive; + } + + public bool IsLoggedOut + { + get => AccountView.AuthStatus == AuthenticationStatus.LoggedOut; + } + + public bool IsLoggedOutAndNotActive + { + get => IsLoggedOut && !IsActive; + } + + public string AuthStatusIconActive + { + get => BitwardenIcons.CheckCircle; + } + + public string AuthStatusIconNotActive + { + get + { + if (IsUnlocked) + { + return BitwardenIcons.Unlock; + } + return BitwardenIcons.Lock; + } + } + } +} diff --git a/src/Maui/Bitwarden/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml b/src/Maui/Bitwarden/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml new file mode 100644 index 000000000..83ac6d937 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Maui/Bitwarden/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml.cs b/src/Maui/Bitwarden/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml.cs new file mode 100644 index 000000000..308d7f84a --- /dev/null +++ b/src/Maui/Bitwarden/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml.cs @@ -0,0 +1,68 @@ +using System; +using Bit.App.Pages; +using Bit.App.Utilities; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public partial class AuthenticatorViewCell : ExtendedGrid + { + public static readonly BindableProperty CipherProperty = BindableProperty.Create( + nameof(Cipher), typeof(CipherView), typeof(AuthenticatorViewCell), default(CipherView), BindingMode.TwoWay); + + public static readonly BindableProperty WebsiteIconsEnabledProperty = BindableProperty.Create( + nameof(WebsiteIconsEnabled), typeof(bool?), typeof(AuthenticatorViewCell)); + + public static readonly BindableProperty TotpSecProperty = BindableProperty.Create( + nameof(TotpSec), typeof(long), typeof(AuthenticatorViewCell)); + + public AuthenticatorViewCell() + { + InitializeComponent(); + } + + public Command CopyCommand { get; set; } + + public CipherView Cipher + { + get => GetValue(CipherProperty) as CipherView; + set => SetValue(CipherProperty, value); + } + + public bool? WebsiteIconsEnabled + { + get => (bool)GetValue(WebsiteIconsEnabledProperty); + set => SetValue(WebsiteIconsEnabledProperty, value); + } + + public long TotpSec + { + get => (long)GetValue(TotpSecProperty); + set => SetValue(TotpSecProperty, value); + } + + public bool ShowIconImage + { + get => WebsiteIconsEnabled ?? false + && !string.IsNullOrWhiteSpace(Cipher.Login?.Uri) + && IconImageSource != null; + } + + private string _iconImageSource = string.Empty; + public string IconImageSource + { + get + { + if (_iconImageSource == string.Empty) // default value since icon source can return null + { + _iconImageSource = IconImageHelper.GetLoginIconImage(Cipher); + } + return _iconImageSource; + } + + } + } +} diff --git a/src/Maui/Bitwarden/Controls/AvatarImageSource.cs b/src/Maui/Bitwarden/Controls/AvatarImageSource.cs new file mode 100644 index 000000000..2c0b54599 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/AvatarImageSource.cs @@ -0,0 +1,181 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Bit.Core.Utilities; +using SkiaSharp; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class AvatarImageSource : StreamImageSource + { + private readonly string _text; + private readonly string _id; + private readonly string _color; + + public override bool Equals(object obj) + { + if (obj is null) + { + return false; + } + + if (obj is AvatarImageSource avatar) + { + return avatar._id == _id && avatar._text == _text && avatar._color == _color; + } + + return base.Equals(obj); + } + + public override int GetHashCode() => _id?.GetHashCode() ?? _text?.GetHashCode() ?? -1; + + public AvatarImageSource(string userId = null, string name = null, string email = null, string color = null) + { + _id = userId; + _text = name; + if (string.IsNullOrWhiteSpace(_text)) + { + _text = email; + } + _color = color; + } + + public override Func> Stream => GetStreamAsync; + + private Task GetStreamAsync(CancellationToken userToken = new CancellationToken()) + { + OnLoadingStarted(); + userToken.Register(CancellationTokenSource.Cancel); + var result = Draw(); + OnLoadingCompleted(CancellationTokenSource.IsCancellationRequested); + return Task.FromResult(result); + } + + private Stream Draw() + { + string chars; + string upperCaseText = null; + + if (string.IsNullOrEmpty(_text)) + { + chars = ".."; + } + else if (_text?.Length > 1) + { + upperCaseText = _text.ToUpper(); + chars = GetFirstLetters(upperCaseText, 2); + } + else + { + chars = upperCaseText = _text.ToUpper(); + } + + var bgColor = _color ?? CoreHelpers.StringToColor(_id ?? upperCaseText, "#33ffffff"); + var textColor = CoreHelpers.TextColorFromBgColor(bgColor); + var size = 50; + + using (var bitmap = new SKBitmap(size * 2, + size * 2, + SKImageInfo.PlatformColorType, + SKAlphaType.Premul)) + { + using (var canvas = new SKCanvas(bitmap)) + { + canvas.Clear(SKColors.Transparent); + using (var paint = new SKPaint + { + IsAntialias = true, + Style = SKPaintStyle.Fill, + StrokeJoin = SKStrokeJoin.Miter, + Color = SKColor.Parse(bgColor) + }) + { + var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2; + var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2; + var radius = midX - midX / 5; + + using (var circlePaint = new SKPaint + { + IsAntialias = true, + Style = SKPaintStyle.Fill, + StrokeJoin = SKStrokeJoin.Miter, + Color = SKColor.Parse(bgColor) + }) + { + canvas.DrawCircle(midX, midY, radius, circlePaint); + + var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal); + var textSize = midX / 1.3f; + using (var textPaint = new SKPaint + { + IsAntialias = true, + Style = SKPaintStyle.Fill, + Color = SKColor.Parse(textColor), + TextSize = textSize, + TextAlign = SKTextAlign.Center, + Typeface = typeface + }) + { + var rect = new SKRect(); + textPaint.MeasureText(chars, ref rect); + canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint); + + using (var img = SKImage.FromBitmap(bitmap)) + { + var data = img.Encode(SKEncodedImageFormat.Png, 100); + return data?.AsStream(true); + } + } + } + } + } + } + } + + private string GetFirstLetters(string data, int charCount) + { + var sanitizedData = data.Trim(); + var parts = sanitizedData.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length > 1 && charCount <= 2) + { + var text = string.Empty; + for (var i = 0; i < charCount; i++) + { + text += parts[i][0]; + } + return text; + } + if (sanitizedData.Length > 2) + { + return sanitizedData.Substring(0, 2); + } + return sanitizedData; + } + + private Color StringToColor(string str) + { + if (str == null) + { + return Color.FromArgb("#33ffffff"); + } + var hash = 0; + for (var i = 0; i < str.Length; i++) + { + hash = str[i] + ((hash << 5) - hash); + } + var color = "#FF"; + for (var i = 0; i < 3; i++) + { + var value = (hash >> (i * 8)) & 0xff; + var base16 = "00" + Convert.ToString(value, 16); + color += base16.Substring(base16.Length - 2); + } + return Color.FromArgb(color); + } + } +} diff --git a/src/Maui/Bitwarden/Controls/AvatarImageSourcePool.cs b/src/Maui/Bitwarden/Controls/AvatarImageSourcePool.cs new file mode 100644 index 000000000..56fcbe886 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/AvatarImageSourcePool.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Concurrent; + +namespace Bit.App.Controls +{ + public interface IAvatarImageSourcePool + { + AvatarImageSource GetOrCreateAvatar(string userId, string name, string email, string color); + } + + public class AvatarImageSourcePool : IAvatarImageSourcePool + { + private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + + public AvatarImageSource GetOrCreateAvatar(string userId, string name, string email, string color) + { + var key = $"{userId}{name}{email}{color}"; + if (!_cache.TryGetValue(key, out var avatar)) + { + avatar = new AvatarImageSource(userId, name, email, color); + if (!_cache.TryAdd(key, avatar) + && + !_cache.TryGetValue(key, out avatar)) // If add fails another thread created the avatar in between the first try get and the try add. + { + // if add and get after fails, then something wrong is going on with this method. + throw new InvalidOperationException("Something is wrong creating the avatar image"); + } + } + return avatar; + } + } +} + diff --git a/src/Maui/Bitwarden/Controls/CipherViewCell/CipherViewCell.xaml b/src/Maui/Bitwarden/Controls/CipherViewCell/CipherViewCell.xaml new file mode 100644 index 000000000..ff5fc17eb --- /dev/null +++ b/src/Maui/Bitwarden/Controls/CipherViewCell/CipherViewCell.xaml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Maui/Bitwarden/Controls/CipherViewCell/CipherViewCell.xaml.cs b/src/Maui/Bitwarden/Controls/CipherViewCell/CipherViewCell.xaml.cs new file mode 100644 index 000000000..1fc0e6797 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/CipherViewCell/CipherViewCell.xaml.cs @@ -0,0 +1,83 @@ +using System; +using System.Windows.Input; +using Bit.App.Abstractions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public partial class CipherViewCell : ExtendedGrid + { + private const int ICON_COLUMN_DEFAULT_WIDTH = 40; + private const int ICON_IMAGE_DEFAULT_WIDTH = 22; + + public static readonly BindableProperty CipherProperty = BindableProperty.Create( + nameof(Cipher), typeof(CipherView), typeof(CipherViewCell), default(CipherView), BindingMode.OneWay); + + public static readonly BindableProperty WebsiteIconsEnabledProperty = BindableProperty.Create( + nameof(WebsiteIconsEnabled), typeof(bool?), typeof(CipherViewCell)); + + public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create( + nameof(ButtonCommand), typeof(ICommand), typeof(CipherViewCell)); + + public CipherViewCell() + { + InitializeComponent(); + + var fontScale = ServiceContainer.Resolve("deviceActionService").GetSystemFontSizeScale(); + _iconColumn.Width = new GridLength(ICON_COLUMN_DEFAULT_WIDTH * fontScale, GridUnitType.Absolute); + _iconImage.WidthRequest = ICON_IMAGE_DEFAULT_WIDTH * fontScale; + _iconImage.HeightRequest = ICON_IMAGE_DEFAULT_WIDTH * fontScale; + } + + public bool? WebsiteIconsEnabled + { + get => (bool)GetValue(WebsiteIconsEnabledProperty); + set => SetValue(WebsiteIconsEnabledProperty, value); + } + + public CipherView Cipher + { + get => GetValue(CipherProperty) as CipherView; + set => SetValue(CipherProperty, value); + } + + public ICommand ButtonCommand + { + get => GetValue(ButtonCommandProperty) as ICommand; + set => SetValue(ButtonCommandProperty, value); + } + + protected override void OnPropertyChanged(string propertyName = null) + { + base.OnPropertyChanged(propertyName); + if (propertyName == CipherProperty.PropertyName) + { + if (Cipher == null) + { + return; + } + BindingContext = new CipherViewCellViewModel(Cipher, WebsiteIconsEnabled ?? false); + } + else if (propertyName == WebsiteIconsEnabledProperty.PropertyName) + { + if (Cipher == null) + { + return; + } + ((CipherViewCellViewModel)BindingContext).WebsiteIconsEnabled = WebsiteIconsEnabled ?? false; + } + } + + private void MoreButton_Clicked(object sender, EventArgs e) + { + var cipher = ((sender as MiButton)?.BindingContext as CipherViewCellViewModel)?.Cipher; + if (cipher != null) + { + ButtonCommand?.Execute(cipher); + } + } + } +} diff --git a/src/Maui/Bitwarden/Controls/CipherViewCell/CipherViewCellViewModel.cs b/src/Maui/Bitwarden/Controls/CipherViewCell/CipherViewCellViewModel.cs new file mode 100644 index 000000000..b5150b003 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/CipherViewCell/CipherViewCellViewModel.cs @@ -0,0 +1,51 @@ +using Bit.App.Utilities; +using Bit.Core.Models.View; +using Bit.Core.Utilities; + +namespace Bit.App.Controls +{ + public class CipherViewCellViewModel : ExtendedViewModel + { + private CipherView _cipher; + private bool _websiteIconsEnabled; + private string _iconImageSource = string.Empty; + + public CipherViewCellViewModel(CipherView cipherView, bool websiteIconsEnabled) + { + Cipher = cipherView; + WebsiteIconsEnabled = websiteIconsEnabled; + } + + public CipherView Cipher + { + get => _cipher; + set => SetProperty(ref _cipher, value); + } + + public bool WebsiteIconsEnabled + { + get => _websiteIconsEnabled; + set => SetProperty(ref _websiteIconsEnabled, value); + } + + public bool ShowIconImage + { + get => WebsiteIconsEnabled + && !string.IsNullOrWhiteSpace(Cipher.LaunchUri) + && IconImageSource != null; + } + + public string IconImageSource + { + get + { + if (_iconImageSource == string.Empty) // default value since icon source can return null + { + _iconImageSource = IconImageHelper.GetIconImage(Cipher); + } + return _iconImageSource; + } + + } + } +} diff --git a/src/Maui/Bitwarden/Controls/CircularProgressbarView.cs b/src/Maui/Bitwarden/Controls/CircularProgressbarView.cs new file mode 100644 index 000000000..f606e51d2 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/CircularProgressbarView.cs @@ -0,0 +1,141 @@ +using System; +using System.Runtime.CompilerServices; +using SkiaSharp; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls; +using Microsoft.Maui; +using SkiaSharp.Views.Maui.Controls; +using SkiaSharp.Views.Maui; + +namespace Bit.App.Controls +{ + public class CircularProgressbarView : SKCanvasView + { + private Circle _circle; + + public static readonly BindableProperty ProgressProperty = BindableProperty.Create( + nameof(Progress), typeof(double), typeof(CircularProgressbarView), propertyChanged: OnProgressChanged); + + public static readonly BindableProperty RadiusProperty = BindableProperty.Create( + nameof(Radius), typeof(float), typeof(CircularProgressbarView), 15f); + + public static readonly BindableProperty StrokeWidthProperty = BindableProperty.Create( + nameof(StrokeWidth), typeof(float), typeof(CircularProgressbarView), 3f); + + public static readonly BindableProperty ProgressColorProperty = BindableProperty.Create( + nameof(ProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.FromArgb("175DDC")); + + public static readonly BindableProperty EndingProgressColorProperty = BindableProperty.Create( + nameof(EndingProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.FromArgb("dd4b39")); + + public static readonly BindableProperty BackgroundProgressColorProperty = BindableProperty.Create( + nameof(BackgroundProgressColor), typeof(Color), typeof(CircularProgressbarView), Colors.White); + + public double Progress + { + get { return (double)GetValue(ProgressProperty); } + set { SetValue(ProgressProperty, value); } + } + + public float Radius + { + get => (float)GetValue(RadiusProperty); + set => SetValue(RadiusProperty, value); + } + public float StrokeWidth + { + get => (float)GetValue(StrokeWidthProperty); + set => SetValue(StrokeWidthProperty, value); + } + + public Color ProgressColor + { + get => (Color)GetValue(ProgressColorProperty); + set => SetValue(ProgressColorProperty, value); + } + + public Color EndingProgressColor + { + get => (Color)GetValue(EndingProgressColorProperty); + set => SetValue(EndingProgressColorProperty, value); + } + + public Color BackgroundProgressColor + { + get => (Color)GetValue(BackgroundProgressColorProperty); + set => SetValue(BackgroundProgressColorProperty, value); + } + + private static void OnProgressChanged(BindableObject bindable, object oldvalue, object newvalue) + { + var context = bindable as CircularProgressbarView; + context.InvalidateSurface(); + } + + protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + base.OnPropertyChanged(propertyName); + if (propertyName == nameof(Progress)) + { + _circle = new Circle(Radius * (float)DeviceDisplay.MainDisplayInfo.Density, (info) => new SKPoint((float)info.Width / 2, (float)info.Height / 2)); + } + } + + protected override void OnPaintSurface(SKPaintSurfaceEventArgs e) + { + base.OnPaintSurface(e); + if (_circle != null) + { + _circle.CalculateCenter(e.Info); + e.Surface.Canvas.Clear(); + DrawCircle(e.Surface.Canvas, _circle, StrokeWidth * (float)DeviceDisplay.MainDisplayInfo.Density, BackgroundProgressColor.ToSKColor()); + DrawArc(e.Surface.Canvas, _circle, () => (float)Progress, StrokeWidth * (float)DeviceDisplay.MainDisplayInfo.Density, ProgressColor.ToSKColor(), EndingProgressColor.ToSKColor()); + } + } + + private void DrawCircle(SKCanvas canvas, Circle circle, float strokewidth, SKColor color) + { + canvas.DrawCircle(circle.Center, circle.Redius, + new SKPaint() + { + StrokeWidth = strokewidth, + Color = color, + IsStroke = true, + IsAntialias = true + }); + } + + private void DrawArc(SKCanvas canvas, Circle circle, Func progress, float strokewidth, SKColor color, SKColor progressEndColor) + { + var progressValue = progress(); + var angle = progressValue * 3.6f; + canvas.DrawArc(circle.Rect, 270, angle, false, + new SKPaint() + { + StrokeWidth = strokewidth, + Color = progressValue < 20f ? progressEndColor : color, + IsStroke = true, + IsAntialias = true + }); + } + } + + public class Circle + { + private readonly Func _centerFunc; + + public Circle(float redius, Func centerFunc) + { + _centerFunc = centerFunc; + Redius = redius; + } + public SKPoint Center { get; set; } + public float Redius { get; set; } + public SKRect Rect => new SKRect(Center.X - Redius, Center.Y - Redius, Center.X + Redius, Center.Y + Redius); + + public void CalculateCenter(SKImageInfo argsInfo) + { + Center = _centerFunc(argsInfo); + } + } +} diff --git a/src/Maui/Bitwarden/Controls/CustomLabel.cs b/src/Maui/Bitwarden/Controls/CustomLabel.cs new file mode 100644 index 000000000..a4e62949b --- /dev/null +++ b/src/Maui/Bitwarden/Controls/CustomLabel.cs @@ -0,0 +1,14 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class CustomLabel : Label + { + public CustomLabel() + { + } + + public int? FontWeight { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Controls/DateTime/DateTimePicker.xaml b/src/Maui/Bitwarden/Controls/DateTime/DateTimePicker.xaml new file mode 100644 index 000000000..e68d97163 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/DateTime/DateTimePicker.xaml @@ -0,0 +1,20 @@ + + + + + diff --git a/src/Maui/Bitwarden/Controls/DateTime/DateTimePicker.xaml.cs b/src/Maui/Bitwarden/Controls/DateTime/DateTimePicker.xaml.cs new file mode 100644 index 000000000..4011b6d3b --- /dev/null +++ b/src/Maui/Bitwarden/Controls/DateTime/DateTimePicker.xaml.cs @@ -0,0 +1,40 @@ +using System.Runtime.CompilerServices; +using Microsoft.Maui.Controls; +using Microsoft.Maui; +using CommunityToolkit.Maui.Converters; +using CommunityToolkit.Maui.ImageSources; +using CommunityToolkit.Maui; +using CommunityToolkit.Maui.Core; +using CommunityToolkit.Maui.Layouts; +using CommunityToolkit.Maui.Views; + +namespace Bit.App.Controls +{ + public partial class DateTimePicker : Grid + { + public DateTimePicker() + { + InitializeComponent(); + } + + protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + base.OnPropertyChanged(propertyName); + + if (propertyName == nameof(BindingContext) + && + BindingContext is DateTimeViewModel dateTimeViewModel) + { + AutomationProperties.SetName(_datePicker, dateTimeViewModel.DateName); + AutomationProperties.SetName(_timePicker, dateTimeViewModel.TimeName); + + _datePicker.PlaceHolder = dateTimeViewModel.DatePlaceholder; + _timePicker.PlaceHolder = dateTimeViewModel.TimePlaceholder; + } + } + } + + public class LazyDateTimePicker : LazyView + { + } +} diff --git a/src/Maui/Bitwarden/Controls/DateTime/DateTimeViewModel.cs b/src/Maui/Bitwarden/Controls/DateTime/DateTimeViewModel.cs new file mode 100644 index 000000000..ac940a773 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/DateTime/DateTimeViewModel.cs @@ -0,0 +1,70 @@ +using System; +using Bit.Core.Utilities; + +namespace Bit.App.Controls +{ + public class DateTimeViewModel : ExtendedViewModel + { + DateTime? _date; + TimeSpan? _time; + + public DateTimeViewModel(string dateName, string timeName) + { + DateName = dateName; + TimeName = timeName; + } + + public Action OnDateChanged { get; set; } + public Action OnTimeChanged { get; set; } + + public DateTime? Date + { + get => _date; + set + { + if (SetProperty(ref _date, value)) + { + OnDateChanged?.Invoke(value); + } + } + } + public TimeSpan? Time + { + get => _time; + set + { + if (SetProperty(ref _time, value)) + { + OnTimeChanged?.Invoke(value); + } + } + } + + public string DateName { get; } + public string TimeName { get; } + + public string DatePlaceholder { get; set; } + public string TimePlaceholder { get; set; } + + public DateTime? DateTime + { + get + { + if (Date.HasValue) + { + if (Time.HasValue) + { + return Date.Value.Add(Time.Value); + } + return Date; + } + return null; + } + set + { + Date = value?.Date; + Time = value?.Date.TimeOfDay; + } + } + } +} diff --git a/src/Maui/Bitwarden/Controls/ExtendedCollectionView.cs b/src/Maui/Bitwarden/Controls/ExtendedCollectionView.cs new file mode 100644 index 000000000..b6e75c4cb --- /dev/null +++ b/src/Maui/Bitwarden/Controls/ExtendedCollectionView.cs @@ -0,0 +1,21 @@ +using System.Linq; +using CommunityToolkit.Maui.Converters; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class ExtendedCollectionView : CollectionView + { + public string ExtraDataForLogging { get; set; } + } + + public class SelectionChangedEventArgsConverter : BaseNullableConverterOneWay + { + public override object? ConvertFrom(SelectionChangedEventArgs? value) + { + return value?.CurrentSelection.FirstOrDefault(); + } + } + +} diff --git a/src/Maui/Bitwarden/Controls/ExtendedDatePicker.cs b/src/Maui/Bitwarden/Controls/ExtendedDatePicker.cs new file mode 100644 index 000000000..ea3e881bb --- /dev/null +++ b/src/Maui/Bitwarden/Controls/ExtendedDatePicker.cs @@ -0,0 +1,87 @@ +using System; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class ExtendedDatePicker : DatePicker + { + private string _format; + + public static readonly BindableProperty PlaceHolderProperty = BindableProperty.Create( + nameof(PlaceHolder), typeof(string), typeof(ExtendedDatePicker)); + + public string PlaceHolder + { + get { return (string)GetValue(PlaceHolderProperty); } + set { SetValue(PlaceHolderProperty, value); } + } + + public static readonly BindableProperty NullableDateProperty = BindableProperty.Create( + nameof(NullableDate), typeof(DateTime?), typeof(ExtendedDatePicker)); + + public DateTime? NullableDate + { + get { return (DateTime?)GetValue(NullableDateProperty); } + set + { + SetValue(NullableDateProperty, value); + UpdateDate(); + } + } + + private void UpdateDate() + { + if (NullableDate.HasValue) + { + if (_format != null) + { + Format = _format; + } + } + else + { + Format = PlaceHolder; + } + } + + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + if (BindingContext != null) + { + _format = Format; + UpdateDate(); + } + } + + protected override void OnPropertyChanged(string propertyName = null) + { + base.OnPropertyChanged(propertyName); + + if (propertyName == DateProperty.PropertyName || (propertyName == IsFocusedProperty.PropertyName && + !IsFocused && (Date.ToString("d") == + DateTime.Now.ToString("d")))) + { + NullableDate = Date; + UpdateDate(); + } + + if (propertyName == NullableDateProperty.PropertyName) + { + if (NullableDate.HasValue) + { + Date = NullableDate.Value; + if (Date.ToString(_format) == DateTime.Now.ToString(_format)) + { + UpdateDate(); + } + } + else + { + UpdateDate(); + } + } + } + } +} diff --git a/src/Maui/Bitwarden/Controls/ExtendedGrid.cs b/src/Maui/Bitwarden/Controls/ExtendedGrid.cs new file mode 100644 index 000000000..922973456 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/ExtendedGrid.cs @@ -0,0 +1,9 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class ExtendedGrid : Grid + { + } +} diff --git a/src/Maui/Bitwarden/Controls/ExtendedSearchBar.cs b/src/Maui/Bitwarden/Controls/ExtendedSearchBar.cs new file mode 100644 index 000000000..db7bb3fd5 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/ExtendedSearchBar.cs @@ -0,0 +1,22 @@ +using Bit.App.Utilities; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class ExtendedSearchBar : SearchBar + { + public ExtendedSearchBar() + { + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + if (Device.RuntimePlatform == Device.iOS) + { + if (ThemeManager.UsingLightTheme) + { + TextColor = Color.Black; + } + } + } + } +} diff --git a/src/Maui/Bitwarden/Controls/ExtendedSlider.cs b/src/Maui/Bitwarden/Controls/ExtendedSlider.cs new file mode 100644 index 000000000..0fe0a833e --- /dev/null +++ b/src/Maui/Bitwarden/Controls/ExtendedSlider.cs @@ -0,0 +1,17 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class ExtendedSlider : Slider + { + public static readonly BindableProperty ThumbBorderColorProperty = BindableProperty.Create( + nameof(ThumbBorderColor), typeof(Color), typeof(ExtendedSlider), Color.FromArgb("b5b5b5")); + + public Color ThumbBorderColor + { + get => (Color)GetValue(ThumbBorderColorProperty); + set => SetValue(ThumbBorderColorProperty, value); + } + } +} diff --git a/src/Maui/Bitwarden/Controls/ExtendedStackLayout.cs b/src/Maui/Bitwarden/Controls/ExtendedStackLayout.cs new file mode 100644 index 000000000..5186fbb0a --- /dev/null +++ b/src/Maui/Bitwarden/Controls/ExtendedStackLayout.cs @@ -0,0 +1,9 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class ExtendedStackLayout : StackLayout + { + } +} diff --git a/src/Maui/Bitwarden/Controls/ExtendedStepper.cs b/src/Maui/Bitwarden/Controls/ExtendedStepper.cs new file mode 100644 index 000000000..b92f12219 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/ExtendedStepper.cs @@ -0,0 +1,27 @@ +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class ExtendedStepper : Stepper + { + public static readonly BindableProperty StepperBackgroundColorProperty = BindableProperty.Create( + nameof(StepperBackgroundColor), typeof(Color), typeof(ExtendedStepper), Colors.White); + + public static readonly BindableProperty StepperForegroundColorProperty = BindableProperty.Create( + nameof(StepperForegroundColor), typeof(Color), typeof(ExtendedStepper), Colors.Black); + + public Color StepperBackgroundColor + { + get => (Color)GetValue(StepperBackgroundColorProperty); + set => SetValue(StepperBackgroundColorProperty, value); + } + + public Color StepperForegroundColor + { + get => (Color)GetValue(StepperForegroundColorProperty); + set => SetValue(StepperForegroundColorProperty, value); + } + } +} diff --git a/src/Maui/Bitwarden/Controls/ExtendedTimePicker.cs b/src/Maui/Bitwarden/Controls/ExtendedTimePicker.cs new file mode 100644 index 000000000..b2985bd1b --- /dev/null +++ b/src/Maui/Bitwarden/Controls/ExtendedTimePicker.cs @@ -0,0 +1,87 @@ +using System; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class ExtendedTimePicker : TimePicker + { + private string _format; + + public static readonly BindableProperty PlaceHolderProperty = BindableProperty.Create( + nameof(PlaceHolder), typeof(string), typeof(ExtendedTimePicker)); + + public string PlaceHolder + { + get { return (string)GetValue(PlaceHolderProperty); } + set { SetValue(PlaceHolderProperty, value); } + } + + public static readonly BindableProperty NullableTimeProperty = BindableProperty.Create( + nameof(NullableTime), typeof(TimeSpan?), typeof(ExtendedTimePicker)); + + public TimeSpan? NullableTime + { + get { return (TimeSpan?)GetValue(NullableTimeProperty); } + set + { + SetValue(NullableTimeProperty, value); + UpdateTime(); + } + } + + private void UpdateTime() + { + if (NullableTime.HasValue) + { + if (_format != null) + { + Format = _format; + } + } + else + { + Format = PlaceHolder; + } + } + + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + if (BindingContext != null) + { + _format = Format; + UpdateTime(); + } + } + + protected override void OnPropertyChanged(string propertyName = null) + { + base.OnPropertyChanged(propertyName); + + if (propertyName == TimeProperty.PropertyName || (propertyName == IsFocusedProperty.PropertyName && + !IsFocused && (Time.ToString("t") == + DateTime.Now.TimeOfDay.ToString("t")))) + { + NullableTime = Time; + UpdateTime(); + } + + if (propertyName == NullableTimeProperty.PropertyName) + { + if (NullableTime.HasValue) + { + Time = NullableTime.Value; + if (Time.ToString(_format) == DateTime.Now.TimeOfDay.ToString(_format)) + { + UpdateTime(); + } + } + else + { + UpdateTime(); + } + } + } + } +} diff --git a/src/Maui/Bitwarden/Controls/ExtendedToolbarItem.cs b/src/Maui/Bitwarden/Controls/ExtendedToolbarItem.cs new file mode 100644 index 000000000..0a004c749 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/ExtendedToolbarItem.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class ExtendedToolbarItem : ToolbarItem + { + public bool UseOriginalImage { get; set; } + + // HACK: For the issue of correctly updating the avatar toolbar item color on iOS + // we need to subscribe to the PropertyChanged event of the ToolbarItem on the CustomNavigationRenderer + // The problem is that there are a lot of private places where the navigation renderer disposes objects + // that we don't have access to, and that we should in order to properly prevent memory leaks + // So as a hack solution we have this OnAppearing/OnDisappearing actions and methods to be called on page lifecycle + // to subscribe/unsubscribe indirectly on the CustomNavigationRenderer + public Action OnAppearingAction { get; set; } + public Action OnDisappearingAction { get; set; } + + public void OnAppearing() + { + OnAppearingAction?.Invoke(); + } + + public void OnDisappearing() + { + OnDisappearingAction?.Invoke(); + } + } +} diff --git a/src/Maui/Bitwarden/Controls/HybridWebView.cs b/src/Maui/Bitwarden/Controls/HybridWebView.cs new file mode 100644 index 000000000..801d9b2f6 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/HybridWebView.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class HybridWebView : View + { + private Action _func; + + public static readonly BindableProperty UriProperty = BindableProperty.Create(propertyName: nameof(Uri), + returnType: typeof(string), declaringType: typeof(HybridWebView), defaultValue: default(string)); + + public string Uri + { + get { return (string)GetValue(UriProperty); } + set { SetValue(UriProperty, value); } + } + + public void RegisterAction(Action callback) + { + _func = callback; + } + + public void Cleanup() + { + _func = null; + } + + public void InvokeAction(string data) + { + _func?.Invoke(data); + } + } +} diff --git a/src/Maui/Bitwarden/Controls/IconButton.cs b/src/Maui/Bitwarden/Controls/IconButton.cs new file mode 100644 index 000000000..d27d50626 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/IconButton.cs @@ -0,0 +1,26 @@ +using Bit.App.Effects; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class IconButton : Button + { + public IconButton() + { + Padding = 0; + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + switch (Device.RuntimePlatform) + { + case Device.iOS: + FontFamily = "bwi-font"; + break; + case Device.Android: + FontFamily = "bwi-font.ttf#bwi-font"; + break; + } + + Effects.Add(new RemoveFontPaddingEffect()); + } + } +} diff --git a/src/Maui/Bitwarden/Controls/IconLabel.cs b/src/Maui/Bitwarden/Controls/IconLabel.cs new file mode 100644 index 000000000..6b5e72308 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/IconLabel.cs @@ -0,0 +1,27 @@ +using Bit.App.Effects; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class IconLabel : Label + { + public bool ShouldUpdateFontSizeDynamicallyForAccesibility { get; set; } + + public IconLabel() + { + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + switch (Device.RuntimePlatform) + { + case Device.iOS: + FontFamily = "bwi-font"; + break; + case Device.Android: + FontFamily = "bwi-font.ttf#bwi-font"; + break; + } + + Effects.Add(new RemoveFontPaddingEffect()); + } + } +} diff --git a/src/Maui/Bitwarden/Controls/IconLabelButton/IconLabelButton.xaml b/src/Maui/Bitwarden/Controls/IconLabelButton/IconLabelButton.xaml new file mode 100644 index 000000000..bb3ce642a --- /dev/null +++ b/src/Maui/Bitwarden/Controls/IconLabelButton/IconLabelButton.xaml @@ -0,0 +1,42 @@ + + + + + + + + + + + + diff --git a/src/Maui/Bitwarden/Controls/IconLabelButton/IconLabelButton.xaml.cs b/src/Maui/Bitwarden/Controls/IconLabelButton/IconLabelButton.xaml.cs new file mode 100644 index 000000000..1fb63785f --- /dev/null +++ b/src/Maui/Bitwarden/Controls/IconLabelButton/IconLabelButton.xaml.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Input; +using Bit.Core.Models.Domain; +using Microsoft.Maui.Controls.Xaml; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public partial class IconLabelButton : Frame + { + public static readonly BindableProperty IconProperty = BindableProperty.Create( + nameof(Icon), typeof(string), typeof(IconLabelButton)); + + public static readonly BindableProperty LabelProperty = BindableProperty.Create( + nameof(Label), typeof(string), typeof(IconLabelButton)); + + public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create( + nameof(ButtonCommand), typeof(ICommand), typeof(IconLabelButton)); + + public static readonly BindableProperty IconLabelColorProperty = BindableProperty.Create( + nameof(IconLabelColor), typeof(Color), typeof(IconLabelButton), Colors.White); + + public static readonly BindableProperty IconLabelBackgroundColorProperty = BindableProperty.Create( + nameof(IconLabelBackgroundColor), typeof(Color), typeof(IconLabelButton), Colors.White); + + public static readonly BindableProperty IconLabelBorderColorProperty = BindableProperty.Create( + nameof(IconLabelBorderColor), typeof(Color), typeof(IconLabelButton), Colors.White); + + public IconLabelButton() + { + InitializeComponent(); + } + + public string Icon + { + get => GetValue(IconProperty) as string; + set => SetValue(IconProperty, value); + } + + public string Label + { + get => GetValue(LabelProperty) as string; + set => SetValue(LabelProperty, value); + } + + public ICommand ButtonCommand + { + get => GetValue(ButtonCommandProperty) as ICommand; + set => SetValue(ButtonCommandProperty, value); + } + + public Color IconLabelColor + { + get { return (Color)GetValue(IconLabelColorProperty); } + set { SetValue(IconLabelColorProperty, value); } + } + + public Color IconLabelBackgroundColor + { + get { return (Color)GetValue(IconLabelBackgroundColorProperty); } + set { SetValue(IconLabelBackgroundColorProperty, value); } + } + + public Color IconLabelBorderColor + { + get { return (Color)GetValue(IconLabelBorderColorProperty); } + set { SetValue(IconLabelBorderColorProperty, value); } + } + } +} + diff --git a/src/Maui/Bitwarden/Controls/MiButton.cs b/src/Maui/Bitwarden/Controls/MiButton.cs new file mode 100644 index 000000000..99309351b --- /dev/null +++ b/src/Maui/Bitwarden/Controls/MiButton.cs @@ -0,0 +1,23 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class MiButton : Button + { + public MiButton() + { + Padding = 0; + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + switch (Device.RuntimePlatform) + { + case Device.iOS: + FontFamily = "Material Icons"; + break; + case Device.Android: + FontFamily = "MaterialIcons_Regular.ttf#Material Icons"; + break; + } + } + } +} diff --git a/src/Maui/Bitwarden/Controls/MiLabel.cs b/src/Maui/Bitwarden/Controls/MiLabel.cs new file mode 100644 index 000000000..4d510899f --- /dev/null +++ b/src/Maui/Bitwarden/Controls/MiLabel.cs @@ -0,0 +1,22 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class MiLabel : Label + { + public MiLabel() + { + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + switch (Device.RuntimePlatform) + { + case Device.iOS: + FontFamily = "Material Icons"; + break; + case Device.Android: + FontFamily = "MaterialIcons_Regular.ttf#Material Icons"; + break; + } + } + } +} diff --git a/src/Maui/Bitwarden/Controls/MonoEntry.cs b/src/Maui/Bitwarden/Controls/MonoEntry.cs new file mode 100644 index 000000000..dfa85d849 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/MonoEntry.cs @@ -0,0 +1,22 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class MonoEntry : Entry + { + public MonoEntry() + { + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + switch (Device.RuntimePlatform) + { + case Device.iOS: + FontFamily = "Menlo-Regular"; + break; + case Device.Android: + FontFamily = "RobotoMono_Regular.ttf#Roboto Mono"; + break; + } + } + } +} diff --git a/src/Maui/Bitwarden/Controls/MonoLabel.cs b/src/Maui/Bitwarden/Controls/MonoLabel.cs new file mode 100644 index 000000000..6baaf399e --- /dev/null +++ b/src/Maui/Bitwarden/Controls/MonoLabel.cs @@ -0,0 +1,22 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class MonoLabel : Label + { + public MonoLabel() + { + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + switch (Device.RuntimePlatform) + { + case Device.iOS: + FontFamily = "Menlo-Regular"; + break; + case Device.Android: + FontFamily = "RobotoMono_Regular.ttf#Roboto Mono"; + break; + } + } + } +} diff --git a/src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/IPasswordStrengthable.cs b/src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/IPasswordStrengthable.cs new file mode 100644 index 000000000..b26c7468c --- /dev/null +++ b/src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/IPasswordStrengthable.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Bit.App.Controls +{ + public interface IPasswordStrengthable + { + string Password { get; } + List UserInputs { get; } + } +} + diff --git a/src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/PasswordStrengthCategory.cs b/src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/PasswordStrengthCategory.cs new file mode 100644 index 000000000..8752b0077 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/PasswordStrengthCategory.cs @@ -0,0 +1,17 @@ +using Bit.Core.Attributes; + +namespace Bit.App.Controls +{ + public enum PasswordStrengthLevel + { + [LocalizableEnum("Weak")] + VeryWeak, + [LocalizableEnum("Weak")] + Weak, + [LocalizableEnum("Good")] + Good, + [LocalizableEnum("Strong")] + Strong + } +} + diff --git a/src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/PasswordStrengthProgressBar.xaml b/src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/PasswordStrengthProgressBar.xaml new file mode 100644 index 000000000..4a578982a --- /dev/null +++ b/src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/PasswordStrengthProgressBar.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + diff --git a/src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/PasswordStrengthProgressBar.xaml.cs b/src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/PasswordStrengthProgressBar.xaml.cs new file mode 100644 index 000000000..d8e5330b8 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/PasswordStrengthProgressBar.xaml.cs @@ -0,0 +1,110 @@ +using System; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public partial class PasswordStrengthProgressBar : StackLayout + { + public static readonly BindableProperty PasswordStrengthLevelProperty = BindableProperty.Create( + nameof(PasswordStrengthLevel), + typeof(PasswordStrengthLevel), + typeof(PasswordStrengthProgressBar), + propertyChanged: OnControlPropertyChanged); + + public static readonly BindableProperty VeryWeakColorProperty = BindableProperty.Create( + nameof(VeryWeakColor), + typeof(Color), + typeof(PasswordStrengthProgressBar), + propertyChanged: OnControlPropertyChanged); + + public static readonly BindableProperty WeakColorProperty = BindableProperty.Create( + nameof(WeakColor), + typeof(Color), + typeof(PasswordStrengthProgressBar), + propertyChanged: OnControlPropertyChanged); + + public static readonly BindableProperty GoodColorProperty = BindableProperty.Create( + nameof(GoodColor), + typeof(Color), + typeof(PasswordStrengthProgressBar), + propertyChanged: OnControlPropertyChanged); + + public static readonly BindableProperty StrongColorProperty = BindableProperty.Create( + nameof(StrongColor), + typeof(Color), + typeof(PasswordStrengthProgressBar), + propertyChanged: OnControlPropertyChanged); + + public PasswordStrengthLevel? PasswordStrengthLevel + { + get { return (PasswordStrengthLevel?)GetValue(PasswordStrengthLevelProperty); } + set { SetValue(PasswordStrengthLevelProperty, value); } + } + + public Color VeryWeakColor + { + get { return (Color)GetValue(VeryWeakColorProperty); } + set { SetValue(VeryWeakColorProperty, value); } + } + + public Color WeakColor + { + get { return (Color)GetValue(WeakColorProperty); } + set { SetValue(WeakColorProperty, value); } + } + + public Color GoodColor + { + get { return (Color)GetValue(GoodColorProperty); } + set { SetValue(GoodColorProperty, value); } + } + + public Color StrongColor + { + get { return (Color)GetValue(StrongColorProperty); } + set { SetValue(StrongColorProperty, value); } + } + + public PasswordStrengthProgressBar() + { + InitializeComponent(); + SetBinding(PasswordStrengthProgressBar.PasswordStrengthLevelProperty, new Binding() { Path = nameof(PasswordStrengthViewModel.PasswordStrengthLevel) }); + UpdateColors(); + } + + private static void OnControlPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + (bindable as PasswordStrengthProgressBar)?.UpdateColors(); + } + + public void UpdateColors() + { + if (_progressBar == null || _progressLabel == null) + { + return; + } + _progressBar.ProgressColor = GetColorForStrength(); + _progressLabel.TextColor = _progressBar.ProgressColor; + } + + private Color GetColorForStrength() + { + switch (PasswordStrengthLevel) + { + case Controls.PasswordStrengthLevel.VeryWeak: + return VeryWeakColor; + case Controls.PasswordStrengthLevel.Weak: + return WeakColor; + case Controls.PasswordStrengthLevel.Good: + return GoodColor; + case Controls.PasswordStrengthLevel.Strong: + return StrongColor; + default: + return Colors.Transparent; + } + } + } +} + diff --git a/src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/PasswordStrengthViewModel.cs b/src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/PasswordStrengthViewModel.cs new file mode 100644 index 000000000..9b9210d21 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/PasswordStrengthProgressBar/PasswordStrengthViewModel.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class PasswordStrengthViewModel : ExtendedViewModel + { + private readonly IPasswordGenerationService _passwordGenerationService; + private readonly IPasswordStrengthable _passwordStrengthable; + private double _passwordStrength; + private Color _passwordColor; + private PasswordStrengthLevel? _passwordStrengthLevel; + + public PasswordStrengthViewModel(IPasswordStrengthable passwordStrengthable) + { + _passwordGenerationService = ServiceContainer.Resolve(); + _passwordStrengthable = passwordStrengthable; + } + + public double PasswordStrength + { + get => _passwordStrength; + set => SetProperty(ref _passwordStrength, value); + } + + public PasswordStrengthLevel? PasswordStrengthLevel + { + get => _passwordStrengthLevel; + set => SetProperty(ref _passwordStrengthLevel, value); + } + + public List GetPasswordStrengthUserInput(string email) => _passwordGenerationService.GetPasswordStrengthUserInput(email); + + public void CalculatePasswordStrength() + { + if (string.IsNullOrEmpty(_passwordStrengthable.Password)) + { + PasswordStrength = 0; + PasswordStrengthLevel = null; + return; + } + + var passwordStrength = _passwordGenerationService.PasswordStrength(_passwordStrengthable.Password, _passwordStrengthable.UserInputs); + // The passwordStrength.Score is 0..4, convertion was made to be used as a progress directly by the control 0..1 + PasswordStrength = (passwordStrength.Score + 1f) / 5f; + if (PasswordStrength <= 0.4f) + { + PasswordStrengthLevel = Controls.PasswordStrengthLevel.VeryWeak; + } + else if (PasswordStrength <= 0.6f) + { + PasswordStrengthLevel = Controls.PasswordStrengthLevel.Weak; + } + else if (PasswordStrength <= 0.8f) + { + PasswordStrengthLevel = Controls.PasswordStrengthLevel.Good; + } + else + { + PasswordStrengthLevel = Controls.PasswordStrengthLevel.Strong; + } + } + } +} + diff --git a/src/Maui/Bitwarden/Controls/RepeaterView.cs b/src/Maui/Bitwarden/Controls/RepeaterView.cs new file mode 100644 index 000000000..3502bb654 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/RepeaterView.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections; +using System.Collections.Specialized; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + [Obsolete] + public class RepeaterView : StackLayout + { + public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create( + nameof(ItemTemplate), typeof(DataTemplate), typeof(RepeaterView), default(DataTemplate)); + + public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create( + nameof(ItemsSource), typeof(ICollection), typeof(RepeaterView), null, BindingMode.OneWay, + propertyChanged: ItemsSourceChanging); + + public RepeaterView() + { + Spacing = 0; + } + + public ICollection ItemsSource + { + get => GetValue(ItemsSourceProperty) as ICollection; + set => SetValue(ItemsSourceProperty, value); + } + + public DataTemplate ItemTemplate + { + get => GetValue(ItemTemplateProperty) as DataTemplate; + set => SetValue(ItemTemplateProperty, value); + } + + private void OnCollectionChanged(object sender, + NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs) + { + Populate(); + } + + protected override void OnPropertyChanged(string propertyName = null) + { + base.OnPropertyChanged(propertyName); + if (propertyName == ItemTemplateProperty.PropertyName || propertyName == ItemsSourceProperty.PropertyName) + { + Populate(); + } + } + + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + Populate(); + } + + protected virtual View ViewFor(object item) + { + View view = null; + var template = ItemTemplate; + if (template != null) + { + if (template is DataTemplateSelector selector) + { + template = selector.SelectTemplate(item, this); + } + var content = template.CreateContent(); + view = content is View ? content as View : ((ViewCell)content).View; + view.BindingContext = item; + } + return view; + } + + private void Populate() + { + if (ItemsSource != null) + { + Children.Clear(); + foreach (var item in ItemsSource) + { + Children.Add(ViewFor(item)); + } + } + } + + private static void ItemsSourceChanging(BindableObject bindable, object oldValue, object newValue) + { + if (oldValue != null && oldValue is INotifyCollectionChanged ov) + { + ov.CollectionChanged -= (bindable as RepeaterView).OnCollectionChanged; + } + if (newValue != null && newValue is INotifyCollectionChanged nv) + { + nv.CollectionChanged += (bindable as RepeaterView).OnCollectionChanged; + } + } + } +} diff --git a/src/Maui/Bitwarden/Controls/SelectableLabel.cs b/src/Maui/Bitwarden/Controls/SelectableLabel.cs new file mode 100644 index 000000000..5ffa4ca52 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/SelectableLabel.cs @@ -0,0 +1,11 @@ +using System; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public class SelectableLabel : Label + { + + } +} diff --git a/src/Maui/Bitwarden/Controls/SendViewCell/SendViewCell.xaml b/src/Maui/Bitwarden/Controls/SendViewCell/SendViewCell.xaml new file mode 100644 index 000000000..948468594 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/SendViewCell/SendViewCell.xaml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Maui/Bitwarden/Controls/SendViewCell/SendViewCell.xaml.cs b/src/Maui/Bitwarden/Controls/SendViewCell/SendViewCell.xaml.cs new file mode 100644 index 000000000..b9ac7cb6a --- /dev/null +++ b/src/Maui/Bitwarden/Controls/SendViewCell/SendViewCell.xaml.cs @@ -0,0 +1,69 @@ +using System; +using Bit.App.Abstractions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Controls +{ + public partial class SendViewCell : ExtendedGrid + { + public static readonly BindableProperty SendProperty = BindableProperty.Create( + nameof(Send), typeof(SendView), typeof(SendViewCell), default(SendView), BindingMode.OneWay); + + public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create( + nameof(ButtonCommand), typeof(Command), typeof(SendViewCell)); + + public static readonly BindableProperty ShowOptionsProperty = BindableProperty.Create( + nameof(ShowOptions), typeof(bool), typeof(SendViewCell), true, BindingMode.OneWay); + + public SendViewCell() + { + InitializeComponent(); + + var deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _iconColumn.Width = new GridLength(40 * deviceActionService.GetSystemFontSizeScale(), GridUnitType.Absolute); + } + + public SendView Send + { + get => GetValue(SendProperty) as SendView; + set => SetValue(SendProperty, value); + } + + public Command ButtonCommand + { + get => GetValue(ButtonCommandProperty) as Command; + set => SetValue(ButtonCommandProperty, value); + } + + public bool ShowOptions + { + get => (bool)GetValue(ShowOptionsProperty); + set => SetValue(ShowOptionsProperty, value); + } + + protected override void OnPropertyChanged(string propertyName = null) + { + base.OnPropertyChanged(propertyName); + if (propertyName == SendProperty.PropertyName) + { + if (Send == null) + { + return; + } + BindingContext = new SendViewCellViewModel(Send, ShowOptions); + } + } + + private void MoreButton_Clicked(object sender, EventArgs e) + { + var send = ((sender as MiButton)?.BindingContext as SendViewCellViewModel)?.Send; + if (send != null) + { + ButtonCommand?.Execute(send); + } + } + } +} diff --git a/src/Maui/Bitwarden/Controls/SendViewCell/SendViewCellViewModel.cs b/src/Maui/Bitwarden/Controls/SendViewCell/SendViewCellViewModel.cs new file mode 100644 index 000000000..63f6f3704 --- /dev/null +++ b/src/Maui/Bitwarden/Controls/SendViewCell/SendViewCellViewModel.cs @@ -0,0 +1,29 @@ +using Bit.Core.Models.View; +using Bit.Core.Utilities; + +namespace Bit.App.Controls +{ + public class SendViewCellViewModel : ExtendedViewModel + { + private SendView _send; + private bool _showOptions; + + public SendViewCellViewModel(SendView sendView, bool showOptions) + { + Send = sendView; + ShowOptions = showOptions; + } + + public SendView Send + { + get => _send; + set => SetProperty(ref _send, value); + } + + public bool ShowOptions + { + get => _showOptions; + set => SetProperty(ref _showOptions, value); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IApiService.cs b/src/Maui/Bitwarden/Core/Abstractions/IApiService.cs new file mode 100644 index 000000000..b073b227c --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IApiService.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; +using Bit.Core.Models.Response; + +namespace Bit.Core.Abstractions +{ + public interface IApiService + { + string ApiBaseUrl { get; set; } + string IdentityBaseUrl { get; set; } + string EventsBaseUrl { get; set; } + bool UrlsSet { get; } + + Task DeleteCipherAsync(string id); + Task DeleteCipherAttachmentAsync(string id, string attachmentId); + Task DeleteFolderAsync(string id); + Task DoRefreshTokenAsync(); + Task GetAccountRevisionDateAsync(); + Task GetActiveBearerTokenAsync(); + Task GetCipherAsync(string id); + Task GetFolderAsync(string id); + Task GetProfileAsync(); + Task GetSyncAsync(); + Task PostAccountKeysAsync(KeysRequest request); + Task PostAccountVerifyPasswordAsync(PasswordVerificationRequest request); + Task PostAccountRequestOTP(); + Task PostAccountVerifyOTPAsync(VerifyOTPRequest request); + Task PostCipherAsync(CipherRequest request); + Task PostCipherCreateAsync(CipherCreateRequest request); + Task PostFolderAsync(FolderRequest request); + Task PostIdentityTokenAsync(TokenRequest request); + Task PostPasswordHintAsync(PasswordHintRequest request); + Task SetPasswordAsync(SetPasswordRequest request); + Task PostPreloginAsync(PreloginRequest request); + Task PostRegisterAsync(RegisterRequest request); + Task PutCipherAsync(string id, CipherRequest request); + Task PutCipherCollectionsAsync(string id, CipherCollectionsRequest request); + Task PutFolderAsync(string id, FolderRequest request); + Task PutShareCipherAsync(string id, CipherShareRequest request); + Task PutDeleteCipherAsync(string id); + Task PutRestoreCipherAsync(string id); + Task RefreshIdentityTokenAsync(); + Task PreValidateSso(string identifier); + Task SendAsync(HttpMethod method, string path, + TRequest body, bool authed, bool hasResponse, Action alterRequest, bool logoutOnUnauthorized = true); + Task SendAsync(HttpRequestMessage requestMessage, CancellationToken cancellationToken = default); + void SetUrls(EnvironmentUrls urls); + [Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")] + Task PostCipherAttachmentLegacyAsync(string id, MultipartFormDataContent data); + Task PostCipherAttachmentAsync(string id, AttachmentRequest request); + Task GetAttachmentData(string cipherId, string attachmentId); + Task PostShareCipherAttachmentAsync(string id, string attachmentId, MultipartFormDataContent data, + string organizationId); + Task RenewAttachmentUploadUrlAsync(string id, string attachmentId); + Task PostAttachmentFileAsync(string id, string attachmentId, MultipartFormDataContent data); + Task> GetHibpBreachAsync(string username); + Task PostTwoFactorEmailAsync(TwoFactorEmailRequest request); + Task PutDeviceTokenAsync(string identifier, DeviceTokenRequest request); + Task PostEventsCollectAsync(IEnumerable request); + Task PutUpdateTempPasswordAsync(UpdateTempPasswordRequest request); + Task PostPasswordAsync(PasswordRequest request); + Task DeleteAccountAsync(DeleteAccountRequest request); + Task GetOrganizationKeysAsync(string id); + Task GetOrganizationAutoEnrollStatusAsync(string identifier); + Task PutOrganizationUserResetPasswordEnrollmentAsync(string orgId, string userId, + OrganizationUserResetPasswordEnrollmentRequest request); + Task GetUserKeyFromKeyConnector(string keyConnectorUrl); + Task PostUserKeyToKeyConnector(string keyConnectorUrl, KeyConnectorUserKeyRequest request); + Task PostSetKeyConnectorKey(SetKeyConnectorKeyRequest request); + Task PostConvertToKeyConnector(); + Task PostLeaveOrganization(string id); + + Task GetSendAsync(string id); + Task PostSendAsync(SendRequest request); + Task PostFileTypeSendAsync(SendRequest request); + Task PostSendFileAsync(string sendId, string fileId, MultipartFormDataContent data); + [Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")] + Task PostSendFileAsync(MultipartFormDataContent data); + Task RenewFileUploadUrlAsync(string sendId, string fileId); + Task PutSendAsync(string id, SendRequest request); + Task PutSendRemovePasswordAsync(string id); + Task DeleteSendAsync(string id); + Task> GetAuthRequestAsync(); + Task GetAuthRequestAsync(string id); + Task GetAuthResponseAsync(string id, string accessCode); + Task PutAuthRequestAsync(string id, string key, string masterPasswordHash, string deviceIdentifier, bool requestApproved); + Task PostCreateRequestAsync(PasswordlessCreateLoginRequest passwordlessCreateLoginRequest); + Task GetKnownDeviceAsync(string email, string deviceIdentifier); + Task GetOrgDomainSsoDetailsAsync(string email); + Task GetConfigsAsync(); + Task GetFastmailAccountIdAsync(string apiKey); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IAppIdService.cs b/src/Maui/Bitwarden/Core/Abstractions/IAppIdService.cs new file mode 100644 index 000000000..6c3bd6d30 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IAppIdService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface IAppIdService + { + Task GetAppIdAsync(); + Task GetAnonymousAppIdAsync(); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IAuditService.cs b/src/Maui/Bitwarden/Core/Abstractions/IAuditService.cs new file mode 100644 index 000000000..047fb7dad --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IAuditService.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Response; + +namespace Bit.Core.Abstractions +{ + public interface IAuditService + { + Task> BreachedAccountsAsync(string username); + Task PasswordLeakedAsync(string password); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IAuthService.cs b/src/Maui/Bitwarden/Core/Abstractions/IAuthService.cs new file mode 100644 index 000000000..e26438743 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IAuthService.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; +using Bit.Core.Models.Response; + +namespace Bit.Core.Abstractions +{ + public interface IAuthService + { + string Email { get; set; } + string MasterPasswordHash { get; set; } + string Code { get; set; } + string CodeVerifier { get; set; } + string SsoRedirectUrl { get; set; } + TwoFactorProviderType? SelectedTwoFactorProviderType { get; set; } + Dictionary TwoFactorProviders { get; set; } + Dictionary> TwoFactorProvidersData { get; set; } + + TwoFactorProviderType? GetDefaultTwoFactorProvider(bool fido2Supported); + bool AuthingWithSso(); + bool AuthingWithPassword(); + List GetSupportedTwoFactorProviders(); + Task LogInAsync(string email, string masterPassword, string captchaToken); + Task LogInSsoAsync(string code, string codeVerifier, string redirectUrl, string orgId); + Task LogInCompleteAsync(string email, string masterPassword, TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null); + Task LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, string captchaToken, bool? remember = null); + Task LogInPasswordlessAsync(string email, string accessCode, string authRequestId, byte[] decryptionKey, string userKeyCiphered, string localHashedPasswordCiphered); + + Task> GetPasswordlessLoginRequestsAsync(); + Task> GetActivePasswordlessLoginRequestsAsync(); + Task GetPasswordlessLoginRequestByIdAsync(string id); + Task GetPasswordlessLoginResponseAsync(string id, string accessCode); + Task PasswordlessLoginAsync(string id, string pubKey, bool requestApproved); + Task PasswordlessCreateLoginRequestAsync(string email); + + void LogOut(Action callback); + void Init(); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IAutofillHandler.cs b/src/Maui/Bitwarden/Core/Abstractions/IAutofillHandler.cs new file mode 100644 index 000000000..84c9489b9 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IAutofillHandler.cs @@ -0,0 +1,16 @@ +using Bit.Core.Models.View; + +namespace Bit.Core.Abstractions +{ + public interface IAutofillHandler + { + bool AutofillServicesEnabled(); + bool SupportsAutofillService(); + void Autofill(CipherView cipher); + void CloseAutofill(); + bool AutofillAccessibilityServiceRunning(); + bool AutofillAccessibilityOverlayPermitted(); + bool AutofillServiceEnabled(); + void DisableAutofillService(); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IAzureFileUpoadService.cs b/src/Maui/Bitwarden/Core/Abstractions/IAzureFileUpoadService.cs new file mode 100644 index 000000000..18f3e2ec0 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IAzureFileUpoadService.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Abstractions +{ + public interface IAzureFileUploadService + { + Task Upload(string uri, EncByteArray data, Func> renewalCallback); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IBiometricService.cs b/src/Maui/Bitwarden/Core/Abstractions/IBiometricService.cs new file mode 100644 index 000000000..232b301ca --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IBiometricService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface IBiometricService + { + Task SetupBiometricAsync(string bioIntegritySrcKey = null); + Task IsSystemBiometricIntegrityValidAsync(string bioIntegritySrcKey = null); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IBroadcasterService.cs b/src/Maui/Bitwarden/Core/Abstractions/IBroadcasterService.cs new file mode 100644 index 000000000..0675aad18 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IBroadcasterService.cs @@ -0,0 +1,13 @@ +using System; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Abstractions +{ + public interface IBroadcasterService + { + void Send(Message message); + void Send(Message message, string id); + void Subscribe(string id, Action messageCallback); + void Unsubscribe(string id); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/ICipherService.cs b/src/Maui/Bitwarden/Core/Abstractions/ICipherService.cs new file mode 100644 index 000000000..d68b0dc93 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/ICipherService.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; + +namespace Bit.Core.Abstractions +{ + public interface ICipherService + { + public enum ShareWithServerError + { + None, + DuplicatedPasskeyInOrg + } + + Task ClearAsync(string userId); + Task ClearCacheAsync(); + Task DeleteAsync(List ids); + Task DeleteAsync(string id); + Task DeleteAttachmentAsync(string id, string attachmentId); + Task DeleteAttachmentWithServerAsync(string id, string attachmentId); + Task DeleteWithServerAsync(string id); + Task EncryptAsync(CipherView model, SymmetricCryptoKey key = null, Cipher originalCipher = null); + Task> GetAllAsync(); + Task> GetAllDecryptedAsync(Func filter = null); + Task, List, List>> GetAllDecryptedByUrlAsync(string url, + List includeOtherTypes = null); + Task> GetAllDecryptedForGroupingAsync(string groupingId, bool folder = true); + Task> GetAllDecryptedForUrlAsync(string url); + Task GetAsync(string id); + Task GetLastUsedForUrlAsync(string url); + Task ReplaceAsync(Dictionary ciphers); + Task SaveAttachmentRawWithServerAsync(Cipher cipher, string filename, byte[] data); + Task SaveCollectionsWithServerAsync(Cipher cipher); + Task SaveNeverDomainAsync(string domain); + Task SaveWithServerAsync(Cipher cipher); + Task ShareWithServerAsync(CipherView cipher, string organizationId, HashSet collectionIds); + Task UpdateLastUsedDateAsync(string id); + Task UpsertAsync(CipherData cipher); + Task UpsertAsync(List cipher); + Task DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId); + Task SoftDeleteWithServerAsync(string id); + Task RestoreWithServerAsync(string id); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IClipboardService.cs b/src/Maui/Bitwarden/Core/Abstractions/IClipboardService.cs new file mode 100644 index 000000000..ebbe2d975 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IClipboardService.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface IClipboardService + { + /// + /// Copies the to the Clipboard. + /// If is set > 0 then the Clipboard will be cleared after this time in milliseconds. + /// if less than 0 then it takes the configuration that the user set in Options. + /// If is true the sensitive flag is passed to the clipdata to obfuscate the + /// clipboard text in the popup (Android 13+ only) + /// + /// Text to be copied to the Clipboard + /// Expiration time in milliseconds of the copied text + /// Flag to mark copied text as sensitive + Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/ICollectionService.cs b/src/Maui/Bitwarden/Core/Abstractions/ICollectionService.cs new file mode 100644 index 000000000..b9d4d7f7d --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/ICollectionService.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; +using CollectionView = Bit.Core.Models.View.CollectionView; + +namespace Bit.Core.Abstractions +{ + public interface ICollectionService + { + Task ClearAsync(string userId); + void ClearCache(); + Task> DecryptManyAsync(List collections); + Task DeleteAsync(string id); + Task EncryptAsync(CollectionView model); + Task> GetAllAsync(); + Task> GetAllDecryptedAsync(); + Task>> GetAllNestedAsync(List collections = null); + Task GetAsync(string id); + Task> GetNestedAsync(string id); + Task ReplaceAsync(Dictionary collections); + Task UpsertAsync(CollectionData collection); + Task UpsertAsync(List collection); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IConditionedAwaiterManager.cs b/src/Maui/Bitwarden/Core/Abstractions/IConditionedAwaiterManager.cs new file mode 100644 index 000000000..ea7dd951b --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IConditionedAwaiterManager.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public enum AwaiterPrecondition + { + EnvironmentUrlsInited + } + + public interface IConditionedAwaiterManager + { + Task GetAwaiterForPrecondition(AwaiterPrecondition awaiterPrecondition); + void SetAsCompleted(AwaiterPrecondition awaiterPrecondition); + void SetException(AwaiterPrecondition awaiterPrecondition, Exception ex); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IConfigService.cs b/src/Maui/Bitwarden/Core/Abstractions/IConfigService.cs new file mode 100644 index 000000000..285288d2f --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IConfigService.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Models.Response; + +namespace Bit.Core.Abstractions +{ + public interface IConfigService + { + Task GetAsync(bool forceRefresh = false); + Task GetFeatureFlagBoolAsync(string key, bool forceRefresh = false, bool defaultValue = false); + Task GetFeatureFlagStringAsync(string key, bool forceRefresh = false, string defaultValue = null); + Task GetFeatureFlagIntAsync(string key, bool forceRefresh = false, int defaultValue = 0); + } +} + diff --git a/src/Maui/Bitwarden/Core/Abstractions/ICryptoFunctionService.cs b/src/Maui/Bitwarden/Core/Abstractions/ICryptoFunctionService.cs new file mode 100644 index 000000000..39b6ba6a1 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/ICryptoFunctionService.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Enums; + +namespace Bit.Core.Abstractions +{ + public interface ICryptoFunctionService + { + Task Pbkdf2Async(string password, string salt, CryptoHashAlgorithm algorithm, int iterations); + Task Pbkdf2Async(byte[] password, string salt, CryptoHashAlgorithm algorithm, int iterations); + Task Pbkdf2Async(string password, byte[] salt, CryptoHashAlgorithm algorithm, int iterations); + Task Pbkdf2Async(byte[] password, byte[] salt, CryptoHashAlgorithm algorithm, int iterations); + Task Argon2Async(string password, string salt, int iterations, int memory, int parallelism); + Task Argon2Async(byte[] password, string salt, int iterations, int memory, int parallelism); + Task Argon2Async(string password, byte[] salt, int iterations, int memory, int parallelism); + Task Argon2Async(byte[] password, byte[] salt, int iterations, int memory, int parallelism); + Task HkdfAsync(byte[] ikm, string salt, string info, int outputByteSize, HkdfAlgorithm algorithm); + Task HkdfAsync(byte[] ikm, byte[] salt, string info, int outputByteSize, HkdfAlgorithm algorithm); + Task HkdfAsync(byte[] ikm, string salt, byte[] info, int outputByteSize, HkdfAlgorithm algorithm); + Task HkdfAsync(byte[] ikm, byte[] salt, byte[] info, int outputByteSize, HkdfAlgorithm algorithm); + Task HkdfExpandAsync(byte[] prk, string info, int outputByteSize, HkdfAlgorithm algorithm); + Task HkdfExpandAsync(byte[] prk, byte[] info, int outputByteSize, HkdfAlgorithm algorithm); + Task HashAsync(string value, CryptoHashAlgorithm algorithm); + Task HashAsync(byte[] value, CryptoHashAlgorithm algorithm); + Task HmacAsync(byte[] value, byte[] key, CryptoHashAlgorithm algorithm); + Task CompareAsync(byte[] a, byte[] b); + Task AesEncryptAsync(byte[] data, byte[] iv, byte[] key); + Task AesDecryptAsync(byte[] data, byte[] iv, byte[] key); + Task RsaEncryptAsync(byte[] data, byte[] publicKey, CryptoHashAlgorithm algorithm); + Task RsaDecryptAsync(byte[] data, byte[] privateKey, CryptoHashAlgorithm algorithm); + Task RsaExtractPublicKeyAsync(byte[] privateKey); + Task> RsaGenerateKeyPairAsync(int length); + Task RandomBytesAsync(int length); + byte[] RandomBytes(int length); + Task RandomNumberAsync(); + uint RandomNumber(); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/ICryptoPrimitiveService.cs b/src/Maui/Bitwarden/Core/Abstractions/ICryptoPrimitiveService.cs new file mode 100644 index 000000000..cf29f01de --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/ICryptoPrimitiveService.cs @@ -0,0 +1,10 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Abstractions +{ + public interface ICryptoPrimitiveService + { + byte[] Pbkdf2(byte[] password, byte[] salt, CryptoHashAlgorithm algorithm, int iterations); + byte[] Argon2id(byte[] password, byte[] salt, int iterations, int memory, int parallelism); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/ICryptoService.cs b/src/Maui/Bitwarden/Core/Abstractions/ICryptoService.cs new file mode 100644 index 000000000..060cd9887 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/ICryptoService.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Response; + +namespace Bit.Core.Abstractions +{ + public interface ICryptoService + { + Task ClearEncKeyAsync(bool memoryOnly = false, string userId = null); + Task ClearKeyAsync(string userId = null); + Task ClearKeyHashAsync(string userId = null); + Task ClearKeyPairAsync(bool memoryOnly = false, string userId = null); + Task ClearKeysAsync(string userId = null); + Task ClearOrgKeysAsync(bool memoryOnly = false, string userId = null); + Task ClearPinProtectedKeyAsync(string userId = null); + void ClearCache(); + Task DecryptFromBytesAsync(byte[] encBytes, SymmetricCryptoKey key); + Task DecryptToBytesAsync(EncString encString, SymmetricCryptoKey key = null); + Task DecryptToUtf8Async(EncString encString, SymmetricCryptoKey key = null); + Task EncryptAsync(byte[] plainValue, SymmetricCryptoKey key = null); + Task EncryptAsync(string plainValue, SymmetricCryptoKey key = null); + Task EncryptToBytesAsync(byte[] plainValue, SymmetricCryptoKey key = null); + Task GetEncKeyAsync(SymmetricCryptoKey key = null); + Task> GetFingerprintAsync(string userId, byte[] publicKey = null); + Task GetKeyAsync(string userId = null); + Task GetKeyHashAsync(); + Task GetOrgKeyAsync(string orgId); + Task> GetOrgKeysAsync(); + Task GetPrivateKeyAsync(); + Task GetPublicKeyAsync(); + Task CompareAndUpdateKeyHashAsync(string masterPassword, SymmetricCryptoKey key); + Task HasEncKeyAsync(); + Task HashPasswordAsync(string password, SymmetricCryptoKey key, HashPurpose hashPurpose = HashPurpose.ServerAuthorization); + Task HasKeyAsync(string userId = null); + Task> MakeEncKeyAsync(SymmetricCryptoKey key); + Task MakeKeyAsync(string password, string salt, KdfConfig config); + Task MakeKeyFromPinAsync(string pin, string salt, KdfConfig config, EncString protectedKeyEs = null); + Task> MakeKeyPairAsync(SymmetricCryptoKey key = null); + Task MakePinKeyAysnc(string pin, string salt, KdfConfig config); + Task> MakeShareKeyAsync(); + Task MakeSendKeyAsync(byte[] keyMaterial); + Task RandomNumberAsync(int min, int max); + Task RandomStringAsync(int length); + Task> RemakeEncKeyAsync(SymmetricCryptoKey key); + Task RsaEncryptAsync(byte[] data, byte[] publicKey = null); + Task RsaDecryptAsync(string encValue, byte[] privateKey = null); + Task SetEncKeyAsync(string encKey); + Task SetEncPrivateKeyAsync(string encPrivateKey); + Task SetKeyAsync(SymmetricCryptoKey key); + Task SetKeyHashAsync(string keyHash); + Task SetOrgKeysAsync(IEnumerable orgs); + Task ToggleKeyAsync(); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IEnvironmentService.cs b/src/Maui/Bitwarden/Core/Abstractions/IEnvironmentService.cs new file mode 100644 index 000000000..7469452b9 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IEnvironmentService.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using Bit.Core.Models.Data; + +namespace Bit.Core.Abstractions +{ + public interface IEnvironmentService + { + string ApiUrl { get; set; } + string BaseUrl { get; set; } + string IconsUrl { get; set; } + string IdentityUrl { get; set; } + string NotificationsUrl { get; set; } + string WebVaultUrl { get; set; } + string EventsUrl { get; set; } + + string GetWebVaultUrl(bool returnNullIfDefault = false); + string GetWebSendUrl(); + Task SetUrlsAsync(EnvironmentUrlData urls); + Task SetUrlsFromStorageAsync(); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IEventService.cs b/src/Maui/Bitwarden/Core/Abstractions/IEventService.cs new file mode 100644 index 000000000..6a8f8f42e --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IEventService.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Bit.Core.Enums; + +namespace Bit.Core.Abstractions +{ + public interface IEventService + { + Task ClearEventsAsync(); + Task CollectAsync(EventType eventType, string cipherId = null, bool uploadImmediately = false); + Task UploadEventsAsync(); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IExportService.cs b/src/Maui/Bitwarden/Core/Abstractions/IExportService.cs new file mode 100644 index 000000000..e43f98644 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IExportService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface IExportService + { + Task GetExport(string format = "csv"); + Task GetOrganizationExport(string organizationId, string format = "csv"); + string GetFileName(string prefix = null, string extension = "csv"); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IFileService.cs b/src/Maui/Bitwarden/Core/Abstractions/IFileService.cs new file mode 100644 index 000000000..6ddf5dabc --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IFileService.cs @@ -0,0 +1,14 @@ +using System; +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface IFileService + { + bool CanOpenFile(string fileName); + bool OpenFile(byte[] fileData, string id, string fileName); + bool SaveFile(byte[] fileData, string id, string fileName, string contentUri); + Task ClearCacheAsync(); + Task SelectFileAsync(); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IFileUploadService.cs b/src/Maui/Bitwarden/Core/Abstractions/IFileUploadService.cs new file mode 100644 index 000000000..75f566550 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IFileUploadService.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Response; + +namespace Bit.Core.Abstractions +{ + public interface IFileUploadService + { + Task UploadCipherAttachmentFileAsync(AttachmentUploadDataResponse uploadData, EncString fileName, EncByteArray encryptedFileData); + Task UploadSendFileAsync(SendFileUploadDataResponse uploadData, EncString fileName, EncByteArray encryptedFileData); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IFolderService.cs b/src/Maui/Bitwarden/Core/Abstractions/IFolderService.cs new file mode 100644 index 000000000..b1feae03d --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IFolderService.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; + +namespace Bit.Core.Abstractions +{ + public interface IFolderService + { + Task ClearAsync(string userId); + void ClearCache(); + Task DeleteAsync(string id); + Task DeleteWithServerAsync(string id); + Task EncryptAsync(FolderView model, SymmetricCryptoKey key = null); + Task> GetAllAsync(); + Task> GetAllDecryptedAsync(); + Task>> GetAllNestedAsync(List folders = null); + Task GetAsync(string id); + Task> GetNestedAsync(string id); + Task ReplaceAsync(Dictionary folders); + Task SaveWithServerAsync(Folder folder); + Task UpsertAsync(FolderData folder); + Task UpsertAsync(List folder); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/II18nService.cs b/src/Maui/Bitwarden/Core/Abstractions/II18nService.cs new file mode 100644 index 000000000..d459966ec --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/II18nService.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Bit.Core.Abstractions +{ + public interface II18nService + { + CultureInfo Culture { get; set; } + StringComparer StringComparer { get; } + Dictionary LocaleNames { get; } + void SetCurrentCulture(CultureInfo culture); + string T(string id, string p1 = null, string p2 = null, string p3 = null); + string Translate(string id, string p1 = null, string p2 = null, string p3 = null); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IKeyConnectorService.cs b/src/Maui/Bitwarden/Core/Abstractions/IKeyConnectorService.cs new file mode 100644 index 000000000..0f88c5e22 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IKeyConnectorService.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Abstractions +{ + public interface IKeyConnectorService + { + Task SetUsesKeyConnector(bool usesKeyConnector); + Task GetUsesKeyConnector(); + Task UserNeedsMigration(); + Task MigrateUser(); + Task GetAndSetKey(string url); + Task GetManagingOrganization(); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/ILogger.cs b/src/Maui/Bitwarden/Core/Abstractions/ILogger.cs new file mode 100644 index 000000000..fbb0d50a2 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/ILogger.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface ILogger + { + /// + /// Place necessary code to initiaze logger + /// + /// + Task InitAsync(); + + /// + /// Returns if the current logger is enable or disable. + /// + /// + Task IsEnabled(); + + /// + /// Changes the state of the current logger. Setting state enabled to false will block logging. + /// + Task SetEnabled(bool value); + + /// + /// Logs something that is not in itself an exception, e.g. a wrong flow or value that needs to be reported + /// and looked into. + /// + /// A text to be used as the issue's title + /// Additional data + void Error(string message, + IDictionary extraData = null, + [CallerMemberName] string memberName = "", + [CallerFilePath] string sourceFilePath = "", + [CallerLineNumber] int sourceLineNumber = 0); + + /// + /// Logs an exception + /// + void Exception(Exception ex); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IMessagingService.cs b/src/Maui/Bitwarden/Core/Abstractions/IMessagingService.cs new file mode 100644 index 000000000..5fb5f987d --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IMessagingService.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Abstractions +{ + public interface IMessagingService + { + void Send(string subscriber, object arg = null); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/INativeLogService.cs b/src/Maui/Bitwarden/Core/Abstractions/INativeLogService.cs new file mode 100644 index 000000000..c24a622ad --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/INativeLogService.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Abstractions +{ + public interface INativeLogService + { + void Debug(string message); + void Error(string message); + void Info(string message); + void Warning(string message); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IOrganizationService.cs b/src/Maui/Bitwarden/Core/Abstractions/IOrganizationService.cs new file mode 100644 index 000000000..fb6c937b7 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IOrganizationService.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Response; + +namespace Bit.Core.Abstractions +{ + public interface IOrganizationService + { + Task GetAsync(string id); + Task GetByIdentifierAsync(string identifier); + Task> GetAllAsync(string userId = null); + Task ReplaceAsync(Dictionary organizations); + Task ClearAllAsync(string userId); + Task GetClaimedOrganizationDomainAsync(string userEmail); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IPasswordGenerationService.cs b/src/Maui/Bitwarden/Core/Abstractions/IPasswordGenerationService.cs new file mode 100644 index 000000000..25062f2ac --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IPasswordGenerationService.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Bit.Core.Models.Domain; +using Zxcvbn; + +namespace Bit.Core.Abstractions +{ + public interface IPasswordGenerationService + { + Task AddHistoryAsync(string password, CancellationToken token = default(CancellationToken)); + Task ClearAsync(string userId = null); + void ClearCache(); + Task GeneratePassphraseAsync(PasswordGenerationOptions options); + Task GeneratePasswordAsync(PasswordGenerationOptions options); + Task> GetHistoryAsync(); + Task<(PasswordGenerationOptions, PasswordGeneratorPolicyOptions)> GetOptionsAsync(); + Result PasswordStrength(string password, List userInputs = null); + Task SaveOptionsAsync(PasswordGenerationOptions options); + void NormalizeOptions(PasswordGenerationOptions options, PasswordGeneratorPolicyOptions enforcedPolicyOptions); + List GetPasswordStrengthUserInput(string email); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IPlatformUtilsService.cs b/src/Maui/Bitwarden/Core/Abstractions/IPlatformUtilsService.cs new file mode 100644 index 000000000..5d8867d54 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IPlatformUtilsService.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; + +namespace Bit.Core.Abstractions +{ + public interface IPlatformUtilsService + { + string GetApplicationVersion(); + /// + /// Gets the device type on the server enum + /// + Bit.Core.Enums.DeviceType GetDevice(); + string GetDeviceString(); + ClientType GetClientType(); + bool IsSelfHost(); + bool IsViewOpen(); + void LaunchUri(string uri, Dictionary options = null); + Task ReadFromClipboardAsync(Dictionary options = null); + Task ShowDialogAsync(string text, string title = null, string confirmText = null, + string cancelText = null, string type = null); + Task ShowPasswordDialogAsync(string title, string body, Func> validator); + Task<(string password, bool valid)> ShowPasswordDialogAndGetItAsync(string title, string body, Func> validator); + void ShowToast(string type, string title, string text, Dictionary options = null); + void ShowToast(string type, string title, string[] text, Dictionary options = null); + void ShowToastForCopiedValue(string valueNameCopied); + bool SupportsFido2(); + bool SupportsDuo(); + Task SupportsBiometricAsync(); + Task IsBiometricIntegrityValidAsync(string bioIntegritySrcKey = null); + Task AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null); + long GetActiveTime(); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IPolicyService.cs b/src/Maui/Bitwarden/Core/Abstractions/IPolicyService.cs new file mode 100644 index 000000000..75ca6168e --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IPolicyService.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Abstractions +{ + public interface IPolicyService + { + void ClearCache(); + Task> GetAll(PolicyType? type, string userId = null); + Task Replace(Dictionary policies, string userId = null); + Task ClearAsync(string userId); + Task GetMasterPasswordPolicyOptions(IEnumerable policies = null, string userId = null); + Task EvaluateMasterPassword(int passwordStrength, string newPassword, + MasterPasswordPolicyOptions enforcedPolicyOptions); + Tuple GetResetPasswordPolicyOptions(IEnumerable policies, + string orgId); + Task PolicyAppliesToUser(PolicyType policyType, Func policyFilter = null, string userId = null); + Task ShouldShowVaultFilterAsync(); + Task GetPasswordGeneratorPolicyOptionsAsync(); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/ISearchService.cs b/src/Maui/Bitwarden/Core/Abstractions/ISearchService.cs new file mode 100644 index 000000000..a13ec0cc6 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/ISearchService.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Bit.Core.Models.View; + +namespace Bit.Core.Abstractions +{ + public interface ISearchService + { + void ClearIndex(); + Task IndexCiphersAsync(); + bool IsSearchable(string query); + Task> SearchCiphersAsync(string query, Func filter = null, + List ciphers = null, CancellationToken ct = default); + List SearchCiphersBasic(List ciphers, string query, + CancellationToken ct = default, bool deleted = false); + Task> SearchSendsAsync(string query, Func filter = null, + List sends = null, CancellationToken ct = default); + List SearchSendsBasic(List sends, string query, + CancellationToken ct = default, bool deleted = false); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/ISendService.cs b/src/Maui/Bitwarden/Core/Abstractions/ISendService.cs new file mode 100644 index 000000000..c6015fa24 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/ISendService.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; + +namespace Bit.Core.Abstractions +{ + public interface ISendService + { + void ClearCache(); + Task<(Send send, EncByteArray encryptedFileData)> EncryptAsync(SendView model, byte[] fileData, string password, + SymmetricCryptoKey key = null); + Task GetAsync(string id); + Task> GetAllAsync(); + Task> GetAllDecryptedAsync(); + Task SaveWithServerAsync(Send sendData, EncByteArray encryptedFileData); + Task UpsertAsync(params SendData[] send); + Task ReplaceAsync(Dictionary sends); + Task ClearAsync(string userId); + Task DeleteAsync(params string[] ids); + Task DeleteWithServerAsync(string id); + Task RemovePasswordWithServerAsync(string id); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/ISettingsService.cs b/src/Maui/Bitwarden/Core/Abstractions/ISettingsService.cs new file mode 100644 index 000000000..8ad100f38 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/ISettingsService.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface ISettingsService + { + Task ClearAsync(string userId); + void ClearCache(); + Task>> GetEquivalentDomainsAsync(); + Task SetEquivalentDomainsAsync(List> equivalentDomains); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IStateMigrationService.cs b/src/Maui/Bitwarden/Core/Abstractions/IStateMigrationService.cs new file mode 100644 index 000000000..00a162011 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IStateMigrationService.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface IStateMigrationService + { + Task MigrateIfNeededAsync(); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IStateService.cs b/src/Maui/Bitwarden/Core/Abstractions/IStateService.cs new file mode 100644 index 000000000..355acd093 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IStateService.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Response; +using Bit.Core.Models.View; +using Bit.Core.Services; + +namespace Bit.Core.Abstractions +{ + public interface IStateService + { + List AccountViews { get; } + Task GetActiveUserIdAsync(); + Task GetActiveUserEmailAsync(); + Task GetActiveUserCustomDataAsync(Func dataMapper); + Task IsActiveAccountAsync(string userId = null); + Task SetActiveUserAsync(string userId); + Task CheckExtensionActiveUserAndSwitchIfNeededAsync(); + Task IsAuthenticatedAsync(string userId = null); + Task GetUserIdAsync(string email); + Task RefreshAccountViewsAsync(bool allowAddAccountRow); + Task AddAccountAsync(Account account); + Task LogoutAccountAsync(string userId, bool userInitiated); + Task GetPreAuthEnvironmentUrlsAsync(); + Task SetPreAuthEnvironmentUrlsAsync(EnvironmentUrlData value); + Task GetEnvironmentUrlsAsync(string userId = null); + Task GetBiometricUnlockAsync(string userId = null); + Task SetBiometricUnlockAsync(bool? value, string userId = null); + Task GetBiometricLockedAsync(string userId = null); + Task SetBiometricLockedAsync(bool value, string userId = null); + Task GetSystemBiometricIntegrityState(string bioIntegritySrcKey); + Task SetSystemBiometricIntegrityState(string bioIntegritySrcKey, string systemBioIntegrityState); + Task IsAccountBiometricIntegrityValidAsync(string bioIntegritySrcKey, string userId = null); + Task SetAccountBiometricIntegrityValidAsync(string bioIntegritySrcKey, string userId = null); + Task CanAccessPremiumAsync(string userId = null); + Task SetPersonalPremiumAsync(bool value, string userId = null); + Task GetProtectedPinAsync(string userId = null); + Task SetProtectedPinAsync(string value, string userId = null); + Task GetPinProtectedAsync(string userId = null); + Task SetPinProtectedAsync(string value, string userId = null); + Task GetPinProtectedKeyAsync(string userId = null); + Task SetPinProtectedKeyAsync(EncString value, string userId = null); + Task SetKdfConfigurationAsync(KdfConfig config, string userId = null); + Task GetKeyEncryptedAsync(string userId = null); + Task SetKeyEncryptedAsync(string value, string userId = null); + Task GetKeyDecryptedAsync(string userId = null); + Task SetKeyDecryptedAsync(SymmetricCryptoKey value, string userId = null); + Task GetKeyHashAsync(string userId = null); + Task SetKeyHashAsync(string value, string userId = null); + Task GetEncKeyEncryptedAsync(string userId = null); + Task SetEncKeyEncryptedAsync(string value, string userId = null); + Task> GetOrgKeysEncryptedAsync(string userId = null); + Task SetOrgKeysEncryptedAsync(Dictionary value, string userId = null); + Task GetPrivateKeyEncryptedAsync(string userId = null); + Task SetPrivateKeyEncryptedAsync(string value, string userId = null); + Task> GetAutofillBlacklistedUrisAsync(string userId = null); + Task SetAutofillBlacklistedUrisAsync(List value, string userId = null); + Task GetAutofillTileAddedAsync(); + Task SetAutofillTileAddedAsync(bool? value); + Task GetEmailAsync(string userId = null); + Task GetNameAsync(string userId = null); + Task SetNameAsync(string value, string userId = null); + Task GetOrgIdentifierAsync(string userId = null); + Task GetLastActiveTimeAsync(string userId = null); + Task SetLastActiveTimeAsync(long? value, string userId = null); + Task GetVaultTimeoutAsync(string userId = null); + Task SetVaultTimeoutAsync(int? value, string userId = null); + Task GetVaultTimeoutActionAsync(string userId = null); + Task SetVaultTimeoutActionAsync(VaultTimeoutAction? value, string userId = null); + Task GetLastFileCacheClearAsync(); + Task SetLastFileCacheClearAsync(DateTime? value); + Task GetPreviousPageInfoAsync(string userId = null); + Task SetPreviousPageInfoAsync(PreviousPageInfo value, string userId = null); + Task GetInvalidUnlockAttemptsAsync(string userId = null); + Task SetInvalidUnlockAttemptsAsync(int? value, string userId = null); + Task GetLastBuildAsync(); + Task SetLastBuildAsync(string value); + Task GetDisableFaviconAsync(); + Task SetDisableFaviconAsync(bool? value); + Task GetDisableAutoTotpCopyAsync(string userId = null); + Task SetDisableAutoTotpCopyAsync(bool? value, string userId = null); + Task GetInlineAutofillEnabledAsync(string userId = null); + Task SetInlineAutofillEnabledAsync(bool? value, string userId = null); + Task GetAutofillDisableSavePromptAsync(string userId = null); + Task SetAutofillDisableSavePromptAsync(bool? value, string userId = null); + Task>> GetLocalDataAsync(string userId = null); + Task SetLocalDataAsync(Dictionary> value, string userId = null); + Task> GetEncryptedCiphersAsync(string userId = null); + Task SetEncryptedCiphersAsync(Dictionary value, string userId = null); + Task GetDefaultUriMatchAsync(string userId = null); + Task SetDefaultUriMatchAsync(int? value, string userId = null); + Task> GetNeverDomainsAsync(string userId = null); + Task SetNeverDomainsAsync(HashSet value, string userId = null); + Task GetClearClipboardAsync(string userId = null); + Task SetClearClipboardAsync(int? value, string userId = null); + Task> GetEncryptedCollectionsAsync(string userId = null); + Task SetEncryptedCollectionsAsync(Dictionary value, string userId = null); + Task GetPasswordRepromptAutofillAsync(string userId = null); + Task SetPasswordRepromptAutofillAsync(bool? value, string userId = null); + Task GetPasswordVerifiedAutofillAsync(string userId = null); + Task SetPasswordVerifiedAutofillAsync(bool? value, string userId = null); + Task GetLastSyncAsync(string userId = null); + Task SetLastSyncAsync(DateTime? value, string userId = null); + Task GetSecurityStampAsync(string userId = null); + Task SetSecurityStampAsync(string value, string userId = null); + Task GetEmailVerifiedAsync(string userId = null); + Task SetEmailVerifiedAsync(bool? value, string userId = null); + Task GetSyncOnRefreshAsync(string userId = null); + Task SetSyncOnRefreshAsync(bool? value, string userId = null); + Task GetRememberedEmailAsync(); + Task SetRememberedEmailAsync(string value); + Task GetRememberedOrgIdentifierAsync(); + Task SetRememberedOrgIdentifierAsync(string value); + Task GetThemeAsync(); + Task SetThemeAsync(string value); + Task GetAutoDarkThemeAsync(); + Task SetAutoDarkThemeAsync(string value); + Task GetAddSitePromptShownAsync(string userId = null); + Task SetAddSitePromptShownAsync(bool? value, string userId = null); + Task GetPushInitialPromptShownAsync(); + Task SetPushInitialPromptShownAsync(bool? value); + Task GetPushLastRegistrationDateAsync(string userId = null); + Task SetPushLastRegistrationDateAsync(DateTime? value, string userId = null); + Task GetPushInstallationRegistrationErrorAsync(); + Task SetPushInstallationRegistrationErrorAsync(string value); + Task GetPushCurrentTokenAsync(string userId = null); + Task SetPushCurrentTokenAsync(string value, string userId = null); + Task> GetEventCollectionAsync(); + Task SetEventCollectionAsync(List value); + Task> GetEncryptedFoldersAsync(string userId = null); + Task SetEncryptedFoldersAsync(Dictionary value, string userId = null); + Task> GetEncryptedPoliciesAsync(string userId = null); + Task SetEncryptedPoliciesAsync(Dictionary value, string userId = null); + Task GetPushRegisteredTokenAsync(); + Task SetPushRegisteredTokenAsync(string value); + Task GetUsesKeyConnectorAsync(string userId = null); + Task SetUsesKeyConnectorAsync(bool? value, string userId = null); + Task GetForcePasswordResetReasonAsync(string userId = null); + Task SetForcePasswordResetReasonAsync(ForcePasswordResetReason? value, string userId = null); + Task> GetOrganizationsAsync(string userId = null); + Task SetOrganizationsAsync(Dictionary organizations, string userId = null); + Task GetPasswordGenerationOptionsAsync(string userId = null); + Task SetPasswordGenerationOptionsAsync(PasswordGenerationOptions value, string userId = null); + Task> GetEncryptedPasswordGenerationHistory(string userId = null); + Task SetEncryptedPasswordGenerationHistoryAsync(List value, string userId = null); + Task> GetEncryptedSendsAsync(string userId = null); + Task SetEncryptedSendsAsync(Dictionary value, string userId = null); + Task> GetSettingsAsync(string userId = null); + Task SetSettingsAsync(Dictionary value, string userId = null); + Task GetAccessTokenAsync(string userId = null); + Task SetAccessTokenAsync(string value, bool skipTokenStorage, string userId = null); + Task GetRefreshTokenAsync(string userId = null); + Task SetRefreshTokenAsync(string value, bool skipTokenStorage, string userId = null); + Task GetTwoFactorTokenAsync(string email = null); + Task SetTwoFactorTokenAsync(string value, string email = null); + Task GetScreenCaptureAllowedAsync(string userId = null); + Task SetScreenCaptureAllowedAsync(bool value, string userId = null); + Task SaveExtensionActiveUserIdToStorageAsync(string userId); + Task GetApprovePasswordlessLoginsAsync(string userId = null); + Task SetApprovePasswordlessLoginsAsync(bool? value, string userId = null); + Task GetPasswordlessLoginNotificationAsync(); + Task SetPasswordlessLoginNotificationAsync(PasswordlessRequestNotification value); + Task GetUsernameGenerationOptionsAsync(string userId = null); + Task SetUsernameGenerationOptionsAsync(UsernameGenerationOptions value, string userId = null); + Task GetShouldConnectToWatchAsync(string userId = null); + Task SetShouldConnectToWatchAsync(bool shouldConnect, string userId = null); + Task GetLastUserShouldConnectToWatchAsync(); + Task SetAvatarColorAsync(string value, string userId = null); + Task GetAvatarColorAsync(string userId = null); + Task GetPreLoginEmailAsync(); + Task SetPreLoginEmailAsync(string value); + string GetLocale(); + void SetLocale(string locale); + ConfigResponse GetConfigs(); + void SetConfigs(ConfigResponse value); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IStorageMediatorService.cs b/src/Maui/Bitwarden/Core/Abstractions/IStorageMediatorService.cs new file mode 100644 index 000000000..27f47c155 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IStorageMediatorService.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Bit.Core.Services; + +namespace Bit.Core.Abstractions +{ + public interface IStorageMediatorService + { + T Get(string key); + void Save(string key, T obj); + void Remove(string key); + + Task GetAsync(string key, bool useSecureStorage = false); + Task SaveAsync(string key, T obj, bool useSecureStorage = false, bool allowSaveNull = false); + Task RemoveAsync(string key, bool useSecureStorage = false); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IStorageService.cs b/src/Maui/Bitwarden/Core/Abstractions/IStorageService.cs new file mode 100644 index 000000000..5ff667cc7 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IStorageService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface IStorageService + { + Task GetAsync(string key); + Task SaveAsync(string key, T obj); + Task RemoveAsync(string key); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/ISyncService.cs b/src/Maui/Bitwarden/Core/Abstractions/ISyncService.cs new file mode 100644 index 000000000..aeb0205b1 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/ISyncService.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Models.Response; + +namespace Bit.Core.Abstractions +{ + public interface ISyncService + { + bool SyncInProgress { get; set; } + Task FullSyncAsync(bool forceSync, bool allowThrowOnError = false); + Task GetLastSyncAsync(); + Task SetLastSyncAsync(DateTime date); + Task SyncDeleteCipherAsync(SyncCipherNotification notification); + Task SyncDeleteFolderAsync(SyncFolderNotification notification); + Task SyncUpsertCipherAsync(SyncCipherNotification notification, bool isEdit); + Task SyncUpsertFolderAsync(SyncFolderNotification notification, bool isEdit); + // Passwordless code will be moved to an independent service in future techdept + Task SyncPasswordlessLoginRequestsAsync(); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/ISynchronousStorageService.cs b/src/Maui/Bitwarden/Core/Abstractions/ISynchronousStorageService.cs new file mode 100644 index 000000000..a0fff8b94 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/ISynchronousStorageService.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Abstractions +{ + public interface ISynchronousStorageService + { + T Get(string key); + void Save(string key, T obj); + void Remove(string key); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/ITokenService.cs b/src/Maui/Bitwarden/Core/Abstractions/ITokenService.cs new file mode 100644 index 000000000..123c60e8a --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/ITokenService.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace Bit.Core.Abstractions +{ + public interface ITokenService + { + Task ClearTokenAsync(string userId = null); + Task ClearTwoFactorTokenAsync(string email); + void ClearCache(); + JObject DecodeToken(); + string GetEmail(); + bool GetEmailVerified(); + string GetIssuer(); + string GetName(); + bool GetPremium(); + Task GetIsExternal(); + Task GetRefreshTokenAsync(); + Task GetTokenAsync(); + Task ToggleTokensAsync(); + DateTime? GetTokenExpirationDate(); + Task GetTwoFactorTokenAsync(string email); + string GetUserId(); + Task SetRefreshTokenAsync(string refreshToken); + Task SetAccessTokenAsync(string token, bool forDecodeOnly = false); + Task SetTokensAsync(string accessToken, string refreshToken); + Task SetTwoFactorTokenAsync(string token, string email); + bool TokenNeedsRefresh(int minutes = 5); + int TokenSecondsRemaining(); + Task PrepareTokenForDecodingAsync(); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/ITotpService.cs b/src/Maui/Bitwarden/Core/Abstractions/ITotpService.cs new file mode 100644 index 000000000..717791b3a --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/ITotpService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface ITotpService + { + Task GetCodeAsync(string key); + int GetTimeInterval(string key); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IUserVerificationService.cs b/src/Maui/Bitwarden/Core/Abstractions/IUserVerificationService.cs new file mode 100644 index 000000000..5f47af210 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IUserVerificationService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Bit.Core.Enums; + +namespace Bit.Core.Abstractions +{ + public interface IUserVerificationService + { + Task VerifyUser(string secret, VerificationType verificationType); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IUsernameGenerationService.cs b/src/Maui/Bitwarden/Core/Abstractions/IUsernameGenerationService.cs new file mode 100644 index 000000000..305c926c3 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IUsernameGenerationService.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Abstractions +{ + public interface IUsernameGenerationService + { + Task GenerateAsync(UsernameGenerationOptions options); + void ClearCache(); + Task GetOptionsAsync(); + Task SaveOptionsAsync(UsernameGenerationOptions options); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IVaultTimeoutService.cs b/src/Maui/Bitwarden/Core/Abstractions/IVaultTimeoutService.cs new file mode 100644 index 000000000..c74001b17 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IVaultTimeoutService.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Enums; + +namespace Bit.Core.Abstractions +{ + public interface IVaultTimeoutService + { + long? DelayLockAndLogoutMs { get; set; } + + Task CheckVaultTimeoutAsync(); + Task ShouldTimeoutAsync(string userId = null); + Task ExecuteTimeoutActionAsync(string userId = null); + Task ClearAsync(string userId = null); + Task IsLockedAsync(string userId = null); + Task ShouldLockAsync(string userId = null); + Task IsLoggedOutByTimeoutAsync(string userId = null); + Task ShouldLogOutByTimeoutAsync(string userId = null); + Task> IsPinLockSetAsync(string userId = null); + Task IsBiometricLockSetAsync(string userId = null); + Task LockAsync(bool allowSoftLock = false, bool userInitiated = false, string userId = null); + Task LogOutAsync(bool userInitiated = true, string userId = null); + Task SetVaultTimeoutOptionsAsync(int? timeout, VaultTimeoutAction? action); + Task GetVaultTimeout(string userId = null); + Task GetVaultTimeoutAction(string userId = null); + } +} diff --git a/src/Maui/Bitwarden/Core/Abstractions/IWatchDeviceService.cs b/src/Maui/Bitwarden/Core/Abstractions/IWatchDeviceService.cs new file mode 100644 index 000000000..0edfb2afe --- /dev/null +++ b/src/Maui/Bitwarden/Core/Abstractions/IWatchDeviceService.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface IWatchDeviceService + { + bool IsConnected { get; } + + Task SetShouldConnectToWatchAsync(bool shouldConnectToWatch); + Task SyncDataToWatchAsync(); + } +} diff --git a/src/Maui/Bitwarden/Core/Attributes/LocalizableEnumAttribute.cs b/src/Maui/Bitwarden/Core/Attributes/LocalizableEnumAttribute.cs new file mode 100644 index 000000000..25230a791 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Attributes/LocalizableEnumAttribute.cs @@ -0,0 +1,14 @@ +using System; +namespace Bit.Core.Attributes +{ + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] + public class LocalizableEnumAttribute : Attribute + { + public LocalizableEnumAttribute(string key) + { + Key = key; + } + + public string Key { get; } + } +} diff --git a/src/Maui/Bitwarden/Core/BitwardenIcons.cs b/src/Maui/Bitwarden/Core/BitwardenIcons.cs new file mode 100644 index 000000000..d9a74e990 --- /dev/null +++ b/src/Maui/Bitwarden/Core/BitwardenIcons.cs @@ -0,0 +1,119 @@ +namespace Bit.Core +{ + public static class BitwardenIcons + { + public const string User = "\xe900"; + public const string UserF = "\xe901"; + public const string Key = "\xe902"; + public const string ShareSquare = "\xe903"; + public const string Hashtag = "\xe904"; + public const string Clone = "\xe905"; + public const string ListAlt = "\xe906"; + public const string IdCard = "\xe907"; + public const string CreditCard = "\xe908"; + public const string Globe = "\xe909"; + public const string StickyNote = "\xe90a"; + public const string Folder = "\xe90b"; + public const string Lock = "\xe90c"; + public const string LockF = "\xe90d"; + public const string Generate = "\xe90e"; + public const string GenerateF = "\xe90f"; + public const string Cog = "\xe910"; + public const string CogF = "\xe911"; + public const string CheckCircle = "\xe912"; + public const string Eye = "\xe913"; + public const string PencilSquare = "\xe914"; + public const string Bookmark = "\xe915"; + public const string Files = "\xe916"; + public const string Trash = "\xe917"; + public const string Plus = "\xe918"; + public const string Star = "\xe919"; + public const string List = "\xe91a"; + public const string AngleRight = "\xe91b"; + public const string ExternalLink = "\xe91c"; + public const string Refresh = "\xe91d"; + public const string Search = "\xe91f"; + public const string Filter = "\xe920"; + public const string PlusCircle = "\xe921"; + public const string UserCircle = "\xe922"; + public const string QuestionCircle = "\xe923"; + public const string Cogs = "\xe924"; + public const string MinusCircle = "\xe925"; + public const string Send = "\xe926"; + public const string SendF = "\xe927"; + public const string Download = "\xe928"; + public const string Pencil = "\xe929"; + public const string SignOut = "\xe92a"; + public const string Share = "\xe92b"; + public const string Clock = "\xe92c"; + public const string AngleDown = "\xe92d"; + public const string CaretDown = "\xe92e"; + public const string Square = "\xe92f"; + public const string Collection = "\xe930"; + public const string Bank = "\xe931"; + public const string Shield = "\xe932"; + public const string Stop = "\xe933"; + public const string PlusSquare = "\xe934"; + public const string Save = "\xe935"; + public const string SignIn = "\xe936"; + public const string Spinner = "\xe937"; + public const string Paypal = "\xe938"; + public const string Dollar = "\xe939"; + public const string Check = "\xe93a"; + public const string CheckSquare = "\xe93b"; + public const string MinusSquare = "\xe93c"; + public const string Close = "\xe93d"; + public const string FolderOpenF = "\xe93e"; + public const string Paperclip = "\xe93f"; + public const string Bitcoin = "\xe940"; + public const string Cut = "\xe941"; + public const string Frown = "\xe942"; + public const string FolderOpen = "\xe943"; + public const string Android = "\xe944"; + public const string Apple = "\xe945"; + public const string Bug = "\xe946"; + public const string ChainBroken = "\xe947"; + public const string Dashboard = "\xe948"; + public const string Envelope = "\xe949"; + public const string ExclamationCircle = "\xe94a"; + public const string ExclamationTriangle = "\xe94b"; + public const string CaretRight = "\xe94c"; + public const string Facebook = "\xe94d"; + public const string FilePdf = "\xe94e"; + public const string FileText = "\xe94f"; + public const string Github = "\xe950"; + public const string Google = "\xe951"; + public const string InfoCircle = "\xe952"; + public const string Lightbulb = "\xe953"; + public const string Link = "\xe954"; + public const string Linkedin = "\xe955"; + public const string Linux = "\xe956"; + public const string LongArrowRight = "\xe957"; + public const string Money = "\xe958"; + public const string Play = "\xe959"; + public const string Reddit = "\xe95a"; + public const string RefreshTab = "\xe95b"; + public const string Sitemap = "\xe95c"; + public const string Sliders = "\xe95d"; + public const string Tag = "\xe95e"; + public const string ThumbTack = "\xe95f"; + public const string ThumbsUp = "\xe960"; + public const string Twitter = "\xe961"; + public const string Unlock = "\xe962"; + public const string Users = "\xe963"; + public const string Windows = "\xe964"; + public const string Wrench = "\xe965"; + public const string Youtube = "\xe966"; + public const string Ban = "\xe967"; + public const string Camera = "\xe968"; + public const string ChevronUp = "\xe969"; + public const string Desktop = "\xe96a"; + public const string EyeSlash = "\xe96d"; + public const string File = "\xe96e"; + public const string Paste = "\xe96f"; + public const string ViewCellMenu = "\xe5d3"; + public const string Device = "\xe986"; + public const string Suitcase = "\xe98c"; + public const string Passkey = "\xe99f"; + } +} diff --git a/src/Maui/Bitwarden/Core/Constants.cs b/src/Maui/Bitwarden/Core/Constants.cs new file mode 100644 index 000000000..6614bab34 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Constants.cs @@ -0,0 +1,128 @@ +namespace Bit.Core +{ + public static class Constants + { + public const int MaxAccounts = 5; + public const int VaultTimeoutDefault = 15; + public const string AndroidAppProtocol = "androidapp://"; + public const string iOSAppProtocol = "iosapp://"; + public const string DefaultUsernameGenerated = "-"; + public const string StateVersionKey = "stateVersion"; + public const string StateKey = "state"; + public const string PreAuthEnvironmentUrlsKey = "preAuthEnvironmentUrls"; + public const string LastFileCacheClearKey = "lastFileCacheClear"; + public const string AutofillTileAdded = "autofillTileAdded"; + public const string PushRegisteredTokenKey = "pushRegisteredToken"; + public const string PushInitialPromptShownKey = "pushInitialPromptShown"; + public const string PushInstallationRegistrationErrorKey = "pushInstallationRegistrationError"; + public const string LastBuildKey = "lastBuild"; + public const string AddSitePromptShownKey = "addSitePromptShown"; + public const string ClearCiphersCacheKey = "clearCiphersCache"; + public const string BiometricIntegritySourceKey = "biometricIntegritySource"; + public const string iOSAutoFillClearCiphersCacheKey = "iOSAutoFillClearCiphersCache"; + public const string iOSAutoFillBiometricIntegritySourceKey = "iOSAutoFillBiometricIntegritySource"; + public const string iOSExtensionClearCiphersCacheKey = "iOSExtensionClearCiphersCache"; + public const string iOSExtensionBiometricIntegritySourceKey = "iOSExtensionBiometricIntegritySource"; + public const string iOSShareExtensionClearCiphersCacheKey = "iOSShareExtensionClearCiphersCache"; + public const string iOSShareExtensionBiometricIntegritySourceKey = "iOSShareExtensionBiometricIntegritySource"; + public const string iOSExtensionActiveUserIdKey = "iOSExtensionActiveUserId"; + public const string EventCollectionKey = "eventCollection"; + public const string RememberedEmailKey = "rememberedEmail"; + public const string RememberedOrgIdentifierKey = "rememberedOrgIdentifier"; + public const string PasswordlessLoginNotificationKey = "passwordlessLoginNotificationKey"; + public const string ThemeKey = "theme"; + public const string AutoDarkThemeKey = "autoDarkTheme"; + public const string DisableFaviconKey = "disableFavicon"; + public const string PasswordlessNotificationId = "26072022"; + public const string AndroidNotificationChannelId = "general_notification_channel"; + public const string iOSNotificationCategoryId = "dismissableCategory"; + public const string iOSNotificationClearActionId = "Clear"; + public const string NotificationData = "notificationData"; + public const string NotificationDataType = "Type"; + public const string PasswordlessLoginRequestKey = "passwordlessLoginRequest"; + public const string PreLoginEmailKey = "preLoginEmailKey"; + public const string ConfigsKey = "configsKey"; + public const string DisplayEuEnvironmentFlag = "display-eu-environment"; + + /// + /// This key is used to store the value of "ShouldConnectToWatch" of the last user that had logged in + /// which is used to handle Apple Watch state logic + /// + public const string LastUserShouldConnectToWatchKey = "lastUserShouldConnectToWatch"; + public const string OtpAuthScheme = "otpauth"; + public const string AppLocaleKey = "appLocale"; + public const string ClearSensitiveFields = "clearSensitiveFields"; + public const string ForceUpdatePassword = "forceUpdatePassword"; + public const int SelectFileRequestCode = 42; + public const int SelectFilePermissionRequestCode = 43; + public const int SaveFileRequestCode = 44; + public const int TotpDefaultTimer = 30; + public const int PasswordlessNotificationTimeoutInMinutes = 15; + public const int Pbkdf2Iterations = 600000; + public const int Argon2Iterations = 3; + public const int Argon2MemoryInMB = 64; + public const int Argon2Parallelism = 4; + public const int MasterPasswordMinimumChars = 12; + public const string DefaultFido2KeyType = "public-key"; + public const string DefaultFido2KeyAlgorithm = "ECDSA"; + public const string DefaultFido2KeyCurve = "P-256"; + + public static readonly string[] AndroidAllClearCipherCacheKeys = + { + ClearCiphersCacheKey + }; + + public static readonly string[] iOSAllClearCipherCacheKeys = + { + ClearCiphersCacheKey, + iOSAutoFillClearCiphersCacheKey, + iOSExtensionClearCiphersCacheKey, + iOSShareExtensionClearCiphersCacheKey + }; + + public static string VaultTimeoutKey(string userId) => $"vaultTimeout_{userId}"; + public static string VaultTimeoutActionKey(string userId) => $"vaultTimeoutAction_{userId}"; + public static string CiphersKey(string userId) => $"ciphers_{userId}"; + public static string FoldersKey(string userId) => $"folders_{userId}"; + public static string CollectionsKey(string userId) => $"collections_{userId}"; + public static string OrganizationsKey(string userId) => $"organizations_{userId}"; + public static string LocalDataKey(string userId) => $"ciphersLocalData_{userId}"; + public static string NeverDomainsKey(string userId) => $"neverDomains_{userId}"; + public static string SendsKey(string userId) => $"sends_{userId}"; + public static string PoliciesKey(string userId) => $"policies_{userId}"; + public static string KeyKey(string userId) => $"key_{userId}"; + public static string EncOrgKeysKey(string userId) => $"encOrgKeys_{userId}"; + public static string EncPrivateKeyKey(string userId) => $"encPrivateKey_{userId}"; + public static string EncKeyKey(string userId) => $"encKey_{userId}"; + public static string KeyHashKey(string userId) => $"keyHash_{userId}"; + public static string PinProtectedKey(string userId) => $"pinProtectedKey_{userId}"; + public static string PassGenOptionsKey(string userId) => $"passwordGenerationOptions_{userId}"; + public static string PassGenHistoryKey(string userId) => $"generatedPasswordHistory_{userId}"; + public static string TwoFactorTokenKey(string email) => $"twoFactorToken_{email}"; + public static string LastActiveTimeKey(string userId) => $"lastActiveTime_{userId}"; + public static string InvalidUnlockAttemptsKey(string userId) => $"invalidUnlockAttempts_{userId}"; + public static string InlineAutofillEnabledKey(string userId) => $"inlineAutofillEnabled_{userId}"; + public static string AutofillDisableSavePromptKey(string userId) => $"autofillDisableSavePrompt_{userId}"; + public static string AutofillBlacklistedUrisKey(string userId) => $"autofillBlacklistedUris_{userId}"; + public static string ClearClipboardKey(string userId) => $"clearClipboard_{userId}"; + public static string SyncOnRefreshKey(string userId) => $"syncOnRefresh_{userId}"; + public static string DefaultUriMatchKey(string userId) => $"defaultUriMatch_{userId}"; + public static string DisableAutoTotpCopyKey(string userId) => $"disableAutoTotpCopy_{userId}"; + public static string PreviousPageKey(string userId) => $"previousPage_{userId}"; + public static string PasswordRepromptAutofillKey(string userId) => $"passwordRepromptAutofillKey_{userId}"; + public static string PasswordVerifiedAutofillKey(string userId) => $"passwordVerifiedAutofillKey_{userId}"; + public static string SettingsKey(string userId) => $"settings_{userId}"; + public static string UsesKeyConnectorKey(string userId) => $"usesKeyConnector_{userId}"; + public static string ProtectedPinKey(string userId) => $"protectedPin_{userId}"; + public static string LastSyncKey(string userId) => $"lastSync_{userId}"; + public static string BiometricUnlockKey(string userId) => $"biometricUnlock_{userId}"; + public static string AccountBiometricIntegrityValidKey(string userId, string systemBioIntegrityState) => + $"accountBiometricIntegrityValid_{userId}_{systemBioIntegrityState}"; + public static string ApprovePasswordlessLoginsKey(string userId) => $"approvePasswordlessLogins_{userId}"; + public static string UsernameGenOptionsKey(string userId) => $"usernameGenerationOptions_{userId}"; + public static string PushLastRegistrationDateKey(string userId) => $"pushLastRegistrationDate_{userId}"; + public static string PushCurrentTokenKey(string userId) => $"pushCurrentToken_{userId}"; + public static string ShouldConnectToWatchKey(string userId) => $"shouldConnectToWatch_{userId}"; + public static string ScreenCaptureAllowedKey(string userId) => $"screenCaptureAllowed_{userId}"; + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/AuthenticationStatus.cs b/src/Maui/Bitwarden/Core/Enums/AuthenticationStatus.cs new file mode 100644 index 000000000..2d81e071d --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/AuthenticationStatus.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Enums +{ + public enum AuthenticationStatus : byte + { + LoggedOut = 0, + Locked = 1, + Unlocked = 2, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/CipherRepromptType.cs b/src/Maui/Bitwarden/Core/Enums/CipherRepromptType.cs new file mode 100644 index 000000000..0e5b60ff2 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/CipherRepromptType.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Enums +{ + public enum CipherRepromptType : byte + { + None = 0, + Password = 1, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/CipherType.cs b/src/Maui/Bitwarden/Core/Enums/CipherType.cs new file mode 100644 index 000000000..cefa35182 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/CipherType.cs @@ -0,0 +1,13 @@ +namespace Bit.Core.Enums +{ + public enum CipherType : byte + { + // Folder is deprecated + //Folder = 0, + Login = 1, + SecureNote = 2, + Card = 3, + Identity = 4, + Fido2Key = 5 + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/ClientType.cs b/src/Maui/Bitwarden/Core/Enums/ClientType.cs new file mode 100644 index 000000000..9d5e4feab --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/ClientType.cs @@ -0,0 +1,36 @@ +namespace Bit.Core.Enums +{ + public enum ClientType : byte + { + Web = 1, + Browser = 2, + Desktop = 3, + Mobile = 4, + Cli = 5, + DirectoryConnector = 6, + } + + public static class ClientTypeExtensions + { + public static string GetString(this ClientType me) + { + switch (me) + { + case ClientType.Web: + return "web"; + case ClientType.Browser: + return "browser"; + case ClientType.Desktop: + return "desktop"; + case ClientType.Mobile: + return "mobile"; + case ClientType.Cli: + return "cli"; + case ClientType.DirectoryConnector: + return "connector"; + default: + return ""; + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/CryptoHashAlgorithm.cs b/src/Maui/Bitwarden/Core/Enums/CryptoHashAlgorithm.cs new file mode 100644 index 000000000..eacdfdfee --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/CryptoHashAlgorithm.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Enums +{ + public enum CryptoHashAlgorithm : byte + { + Sha1 = 0, + Sha256 = 1, + Sha512 = 2, + Md5 = 3 + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/DeviceType.cs b/src/Maui/Bitwarden/Core/Enums/DeviceType.cs new file mode 100644 index 000000000..69b740756 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/DeviceType.cs @@ -0,0 +1,27 @@ +namespace Bit.Core.Enums +{ + public enum DeviceType : byte + { + Android = 0, + iOS = 1, + ChromeExtension = 2, + FirefoxExtension = 3, + OperaExtension = 4, + EdgeExtension = 5, + WindowsDesktop = 6, + MacOsDesktop = 7, + LinuxDesktop = 8, + ChromeBrowser = 9, + FirefoxBrowser = 10, + OperaBrowser = 11, + EdgeBrowser = 12, + IEBrowser = 13, + UnknownBrowser = 14, + AndroidAmazon = 15, + UWP = 16, + SafariBrowser = 17, + VivaldiBrowser = 18, + VivaldiExtension = 19, + SafariExtension = 20 + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/EncryptionType.cs b/src/Maui/Bitwarden/Core/Enums/EncryptionType.cs new file mode 100644 index 000000000..2b6eaf086 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/EncryptionType.cs @@ -0,0 +1,13 @@ +namespace Bit.Core.Enums +{ + public enum EncryptionType : byte + { + AesCbc256_B64 = 0, + AesCbc128_HmacSha256_B64 = 1, + AesCbc256_HmacSha256_B64 = 2, + Rsa2048_OaepSha256_B64 = 3, + Rsa2048_OaepSha1_B64 = 4, + Rsa2048_OaepSha256_HmacSha256_B64 = 5, + Rsa2048_OaepSha1_HmacSha256_B64 = 6 + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/EventType.cs b/src/Maui/Bitwarden/Core/Enums/EventType.cs new file mode 100644 index 000000000..3e2c3f722 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/EventType.cs @@ -0,0 +1,51 @@ +namespace Bit.Core.Enums +{ + public enum EventType : int + { + User_LoggedIn = 1000, + User_ChangedPassword = 1001, + User_Updated2fa = 1002, + User_Disabled2fa = 1003, + User_Recovered2fa = 1004, + User_FailedLogIn = 1005, + User_FailedLogIn2fa = 1006, + User_ClientExportedVault = 1007, + + Cipher_Created = 1100, + Cipher_Updated = 1101, + Cipher_Deleted = 1102, + Cipher_AttachmentCreated = 1103, + Cipher_AttachmentDeleted = 1104, + Cipher_Shared = 1105, + Cipher_UpdatedCollections = 1106, + Cipher_ClientViewed = 1107, + Cipher_ClientToggledPasswordVisible = 1108, + Cipher_ClientToggledHiddenFieldVisible = 1109, + Cipher_ClientToggledCardCodeVisible = 1110, + Cipher_ClientCopiedPassword = 1111, + Cipher_ClientCopiedHiddenField = 1112, + Cipher_ClientCopiedCardCode = 1113, + Cipher_ClientAutofilled = 1114, + Cipher_SoftDeleted = 1115, + Cipher_Restored = 1116, + Cipher_ClientToggledCardNumberVisible = 1117, + + Collection_Created = 1300, + Collection_Updated = 1301, + Collection_Deleted = 1302, + + Group_Created = 1400, + Group_Updated = 1401, + Group_Deleted = 1402, + + OrganizationUser_Invited = 1500, + OrganizationUser_Confirmed = 1501, + OrganizationUser_Updated = 1502, + OrganizationUser_Removed = 1503, + OrganizationUser_UpdatedGroups = 1504, + + Organization_Updated = 1600, + Organization_PurgedVault = 1601, + // Organization_ClientExportedVault = 1602, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/FieldType.cs b/src/Maui/Bitwarden/Core/Enums/FieldType.cs new file mode 100644 index 000000000..5eef485b7 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/FieldType.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Enums +{ + public enum FieldType : byte + { + Text = 0, + Hidden = 1, + Boolean = 2, + Linked = 3, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/FileUploadType.cs b/src/Maui/Bitwarden/Core/Enums/FileUploadType.cs new file mode 100644 index 000000000..8aeb51072 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/FileUploadType.cs @@ -0,0 +1,9 @@ +using System; +namespace Bit.Core.Enums +{ + public enum FileUploadType + { + Direct = 0, + Azure = 1, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/ForwardedEmailServiceType.cs b/src/Maui/Bitwarden/Core/Enums/ForwardedEmailServiceType.cs new file mode 100644 index 000000000..71bb25048 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/ForwardedEmailServiceType.cs @@ -0,0 +1,19 @@ +using Bit.Core.Attributes; + +namespace Bit.Core.Enums +{ + public enum ForwardedEmailServiceType + { + None = -1, + [LocalizableEnum("AnonAddy")] + AnonAddy = 0, + [LocalizableEnum("FirefoxRelay")] + FirefoxRelay = 1, + [LocalizableEnum("SimpleLogin")] + SimpleLogin = 2, + [LocalizableEnum("DuckDuckGo")] + DuckDuckGo = 3, + [LocalizableEnum("Fastmail")] + Fastmail = 4, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/GeneratorType.cs b/src/Maui/Bitwarden/Core/Enums/GeneratorType.cs new file mode 100644 index 000000000..22c166e41 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/GeneratorType.cs @@ -0,0 +1,12 @@ +using Bit.Core.Attributes; + +namespace Bit.Core.Enums +{ + public enum GeneratorType + { + [LocalizableEnum("Password")] + Password = 0, + [LocalizableEnum("Username")] + Username = 1 + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/HashPurpose.cs b/src/Maui/Bitwarden/Core/Enums/HashPurpose.cs new file mode 100644 index 000000000..c37779d14 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/HashPurpose.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Enums +{ + public enum HashPurpose : byte + { + ServerAuthorization = 1, + LocalAuthorization = 2, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/HdkfAlgorithm.cs b/src/Maui/Bitwarden/Core/Enums/HdkfAlgorithm.cs new file mode 100644 index 000000000..0f2b32e17 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/HdkfAlgorithm.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Enums +{ + public enum HkdfAlgorithm : byte + { + Sha256 = 1, + Sha512 = 2, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/KdfType.cs b/src/Maui/Bitwarden/Core/Enums/KdfType.cs new file mode 100644 index 000000000..86cdf849c --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/KdfType.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Enums +{ + public enum KdfType : byte + { + PBKDF2_SHA256 = 0, + Argon2id = 1, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/LinkedIdType.cs b/src/Maui/Bitwarden/Core/Enums/LinkedIdType.cs new file mode 100644 index 000000000..2ffd0acb5 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/LinkedIdType.cs @@ -0,0 +1,39 @@ +namespace Bit.Core.Enums +{ + + public enum LinkedIdType : int + { + // Login + Login_Username = 100, + Login_Password = 101, + + // Card + Card_CardholderName = 300, + Card_ExpMonth = 301, + Card_ExpYear = 302, + Card_Code = 303, + Card_Brand = 304, + Card_Number = 305, + + // Identity + Identity_Title = 400, + Identity_MiddleName = 401, + Identity_Address1 = 402, + Identity_Address2 = 403, + Identity_Address3 = 404, + Identity_City = 405, + Identity_State = 406, + Identity_PostalCode = 407, + Identity_Country = 408, + Identity_Company = 409, + Identity_Email = 410, + Identity_Phone = 411, + Identity_Ssn = 412, + Identity_Username = 413, + Identity_PassportNumber = 414, + Identity_LicenseNumber = 415, + Identity_FirstName = 416, + Identity_LastName = 417, + Identity_FullName = 418, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/NavigationTarget.cs b/src/Maui/Bitwarden/Core/Enums/NavigationTarget.cs new file mode 100644 index 000000000..8cb83ebf5 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/NavigationTarget.cs @@ -0,0 +1,14 @@ +namespace Bit.Core.Enums +{ + public enum NavigationTarget + { + HomeLogin, + Login, + Lock, + Home, + AddEditCipher, + AutofillCiphers, + SendAddEdit, + OtpCipherSelection + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/NotificationType.cs b/src/Maui/Bitwarden/Core/Enums/NotificationType.cs new file mode 100644 index 000000000..e99439622 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/NotificationType.cs @@ -0,0 +1,27 @@ +namespace Bit.Core.Enums +{ + public enum NotificationType : byte + { + SyncCipherUpdate = 0, + SyncCipherCreate = 1, + SyncLoginDelete = 2, + SyncFolderDelete = 3, + SyncCiphers = 4, + + SyncVault = 5, + SyncOrgKeys = 6, + SyncFolderCreate = 7, + SyncFolderUpdate = 8, + SyncCipherDelete = 9, + SyncSettings = 10, + + LogOut = 11, + + SyncSendCreate = 12, + SyncSendUpdate = 13, + SyncSendDelete = 14, + + AuthRequest = 15, + AuthRequestResponse = 16, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/OrganizationUserStatusType.cs b/src/Maui/Bitwarden/Core/Enums/OrganizationUserStatusType.cs new file mode 100644 index 000000000..7d2246dc8 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/OrganizationUserStatusType.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Enums +{ + public enum OrganizationUserStatusType : byte + { + Invited = 0, + Accepted = 1, + Confirmed = 2 + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/OrganizationUserType.cs b/src/Maui/Bitwarden/Core/Enums/OrganizationUserType.cs new file mode 100644 index 000000000..738c80657 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/OrganizationUserType.cs @@ -0,0 +1,11 @@ +namespace Bit.Core.Enums +{ + public enum OrganizationUserType : byte + { + Owner = 0, + Admin = 1, + User = 2, + Manager = 3, + Custom = 4, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/PaymentMethodType.cs b/src/Maui/Bitwarden/Core/Enums/PaymentMethodType.cs new file mode 100644 index 000000000..0821f211b --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/PaymentMethodType.cs @@ -0,0 +1,12 @@ +namespace Bit.Core.Enums +{ + public enum PaymentMethodType : byte + { + Card = 0, + BankAccount = 1, + PayPal = 2, + BitPay = 3, + Credit = 4, + WireTransfer = 5, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/PlanType.cs b/src/Maui/Bitwarden/Core/Enums/PlanType.cs new file mode 100644 index 000000000..4381ec511 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/PlanType.cs @@ -0,0 +1,13 @@ +namespace Bit.Core.Enums +{ + public enum PlanType : byte + { + Free = 0, + FamiliesAnnually = 1, + TeamsMonthly = 2, + TeamsAnnually = 3, + EnterpriseMonthly = 4, + EnterpriseAnnually = 5, + Custom = 6 + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/PolicyType.cs b/src/Maui/Bitwarden/Core/Enums/PolicyType.cs new file mode 100644 index 000000000..59c0566a8 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/PolicyType.cs @@ -0,0 +1,17 @@ +namespace Bit.Core.Enums +{ + public enum PolicyType : byte + { + TwoFactorAuthentication = 0, // Requires users to have 2fa enabled + MasterPassword = 1, // Sets minimum requirements for master password complexity + PasswordGenerator = 2, // Sets minimum requirements/default type for generated passwords/passphrases + OnlyOrg = 3, // Allows users to only be apart of one organization + RequireSso = 4, // Requires users to authenticate with SSO + PersonalOwnership = 5, // Disables personal vault ownership for adding/cloning items + DisableSend = 6, // Disables the ability to create and edit Sends + SendOptions = 7, // Sets restrictions or defaults for Bitwarden Sends + ResetPassword = 8, // Allows orgs to use reset password : also can enable auto-enrollment during invite flow + MaximumVaultTimeout = 9, // Sets the maximum allowed vault timeout + DisablePersonalVaultExport = 10, // Disable personal vault export + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/SecureNoteType.cs b/src/Maui/Bitwarden/Core/Enums/SecureNoteType.cs new file mode 100644 index 000000000..cc84edfc3 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/SecureNoteType.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Enums +{ + public enum SecureNoteType : byte + { + Generic = 0 + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/SendType.cs b/src/Maui/Bitwarden/Core/Enums/SendType.cs new file mode 100644 index 000000000..fb447b2b5 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/SendType.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Enums +{ + public enum SendType + { + Text = 0, + File = 1, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/StorageLocation.cs b/src/Maui/Bitwarden/Core/Enums/StorageLocation.cs new file mode 100644 index 000000000..faa9476c4 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/StorageLocation.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Enums +{ + public enum StorageLocation + { + Both = 0, + Disk = 1, + Memory = 2 + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/TransactionType.cs b/src/Maui/Bitwarden/Core/Enums/TransactionType.cs new file mode 100644 index 000000000..45baa68c0 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/TransactionType.cs @@ -0,0 +1,11 @@ +namespace Bit.Core.Enums +{ + public enum TransactionType : byte + { + Charge = 0, + Credit = 1, + PromotionalCredit = 2, + ReferralCredit = 3, + Refund = 4, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/TwoFactorProviderType.cs b/src/Maui/Bitwarden/Core/Enums/TwoFactorProviderType.cs new file mode 100644 index 000000000..70ac4de0f --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/TwoFactorProviderType.cs @@ -0,0 +1,14 @@ +namespace Bit.Core.Enums +{ + public enum TwoFactorProviderType : byte + { + Authenticator = 0, + Email = 1, + Duo = 2, + YubiKey = 3, + U2f = 4, + Remember = 5, + OrganizationDuo = 6, + Fido2WebAuthn = 7, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/UriMatchType.cs b/src/Maui/Bitwarden/Core/Enums/UriMatchType.cs new file mode 100644 index 000000000..569437298 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/UriMatchType.cs @@ -0,0 +1,12 @@ +namespace Bit.Core.Enums +{ + public enum UriMatchType : byte + { + Domain = 0, + Host = 1, + StartsWith = 2, + Exact = 3, + RegularExpression = 4, + Never = 5 + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/UsernameEmailType.cs b/src/Maui/Bitwarden/Core/Enums/UsernameEmailType.cs new file mode 100644 index 000000000..0979672f0 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/UsernameEmailType.cs @@ -0,0 +1,12 @@ +using Bit.Core.Attributes; + +namespace Bit.Core.Enums +{ + public enum UsernameEmailType + { + [LocalizableEnum("Random")] + Random = 0, + [LocalizableEnum("Website")] + Website = 1, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/UsernameType.cs b/src/Maui/Bitwarden/Core/Enums/UsernameType.cs new file mode 100644 index 000000000..e5e286236 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/UsernameType.cs @@ -0,0 +1,16 @@ +using Bit.Core.Attributes; + +namespace Bit.Core.Enums +{ + public enum UsernameType + { + [LocalizableEnum("PlusAddressedEmail")] + PlusAddressedEmail = 0, + [LocalizableEnum("CatchAllEmail")] + CatchAllEmail = 1, + [LocalizableEnum("ForwardedEmailAlias")] + ForwardedEmailAlias = 2, + [LocalizableEnum("RandomWord")] + RandomWord = 3, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/VaultTimeoutAction.cs b/src/Maui/Bitwarden/Core/Enums/VaultTimeoutAction.cs new file mode 100644 index 000000000..5a9c33909 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/VaultTimeoutAction.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Enums +{ + public enum VaultTimeoutAction + { + Lock = 0, + Logout = 1, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/VerificationType.cs b/src/Maui/Bitwarden/Core/Enums/VerificationType.cs new file mode 100644 index 000000000..8037a0465 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/VerificationType.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Enums +{ + public enum VerificationType + { + MasterPassword = 0, + OTP = 1, + } +} diff --git a/src/Maui/Bitwarden/Core/Enums/WatchState.cs b/src/Maui/Bitwarden/Core/Enums/WatchState.cs new file mode 100644 index 000000000..8452e8d68 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Enums/WatchState.cs @@ -0,0 +1,13 @@ +namespace Bit.Core.Enums +{ + public enum WatchState : byte + { + Valid = 0, + NeedLogin, + NeedPremium, + NeedSetup, + Need2FAItem, + Syncing + //NeedUnlock + } +} diff --git a/src/Maui/Bitwarden/Core/Exceptions/ApiException.cs b/src/Maui/Bitwarden/Core/Exceptions/ApiException.cs new file mode 100644 index 000000000..9d0a774b4 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Exceptions/ApiException.cs @@ -0,0 +1,22 @@ +using System; +using Bit.Core.Models.Response; + +namespace Bit.Core.Exceptions +{ + public class ApiException : Exception + { + public ApiException() + : base("An API error has occurred.") + { } + + public ApiException(ErrorResponse error) + : this() + { + Error = error; + } + + public ErrorResponse Error { get; set; } + + public override string Message => Error?.GetFullMessage() ?? base.Message; + } +} diff --git a/src/Maui/Bitwarden/Core/Exceptions/ForwardedEmailInvalidSecretException.cs b/src/Maui/Bitwarden/Core/Exceptions/ForwardedEmailInvalidSecretException.cs new file mode 100644 index 000000000..2871b60d2 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Exceptions/ForwardedEmailInvalidSecretException.cs @@ -0,0 +1,11 @@ +using System; +namespace Bit.Core.Exceptions +{ + public class ForwardedEmailInvalidSecretException : Exception + { + public ForwardedEmailInvalidSecretException(Exception innerEx) + : base("Invalid API Secret", innerEx) + { + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Api/CardApi.cs b/src/Maui/Bitwarden/Core/Models/Api/CardApi.cs new file mode 100644 index 000000000..4d5830363 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Api/CardApi.cs @@ -0,0 +1,12 @@ +namespace Bit.Core.Models.Api +{ + public class CardApi + { + public string CardholderName { get; set; } + public string Brand { get; set; } + public string Number { get; set; } + public string ExpMonth { get; set; } + public string ExpYear { get; set; } + public string Code { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Api/Fido2KeyApi.cs b/src/Maui/Bitwarden/Core/Models/Api/Fido2KeyApi.cs new file mode 100644 index 000000000..61f2444af --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Api/Fido2KeyApi.cs @@ -0,0 +1,37 @@ +using Bit.Core.Models.Domain; +using Bit.Core.Models.Export; + +namespace Bit.Core.Models.Api +{ + public class Fido2KeyApi + { + public Fido2KeyApi() + { + } + + public Fido2KeyApi(Fido2Key fido2Key) + { + NonDiscoverableId = fido2Key.NonDiscoverableId?.EncryptedString; + KeyType = fido2Key.KeyType?.EncryptedString; + KeyAlgorithm = fido2Key.KeyAlgorithm?.EncryptedString; + KeyCurve = fido2Key.KeyCurve?.EncryptedString; + KeyValue = fido2Key.KeyValue?.EncryptedString; + RpId = fido2Key.RpId?.EncryptedString; + RpName = fido2Key.RpName?.EncryptedString; + UserHandle = fido2Key.UserHandle?.EncryptedString; + UserName = fido2Key.UserName?.EncryptedString; + Counter = fido2Key.Counter?.EncryptedString; + } + + public string NonDiscoverableId { get; set; } + public string KeyType { get; set; } = Constants.DefaultFido2KeyType; + public string KeyAlgorithm { get; set; } = Constants.DefaultFido2KeyAlgorithm; + public string KeyCurve { get; set; } = Constants.DefaultFido2KeyCurve; + public string KeyValue { get; set; } + public string RpId { get; set; } + public string RpName { get; set; } + public string UserHandle { get; set; } + public string UserName { get; set; } + public string Counter { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Api/FieldApi.cs b/src/Maui/Bitwarden/Core/Models/Api/FieldApi.cs new file mode 100644 index 000000000..524d13ecf --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Api/FieldApi.cs @@ -0,0 +1,12 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Models.Api +{ + public class FieldApi + { + public FieldType Type { get; set; } + public string Name { get; set; } + public string Value { get; set; } + public LinkedIdType? LinkedId { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Api/IdentityApi.cs b/src/Maui/Bitwarden/Core/Models/Api/IdentityApi.cs new file mode 100644 index 000000000..b7a9cfd11 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Api/IdentityApi.cs @@ -0,0 +1,24 @@ +namespace Bit.Core.Models.Api +{ + public class IdentityApi + { + public string Title { get; set; } + public string FirstName { get; set; } + public string MiddleName { get; set; } + public string LastName { get; set; } + public string Address1 { get; set; } + public string Address2 { get; set; } + public string Address3 { get; set; } + public string City { get; set; } + public string State { get; set; } + public string PostalCode { get; set; } + public string Country { get; set; } + public string Company { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public string SSN { get; set; } + public string Username { get; set; } + public string PassportNumber { get; set; } + public string LicenseNumber { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Api/LoginApi.cs b/src/Maui/Bitwarden/Core/Models/Api/LoginApi.cs new file mode 100644 index 000000000..42d32f343 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Api/LoginApi.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace Bit.Core.Models.Api +{ + public class LoginApi + { + public List Uris { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public DateTime? PasswordRevisionDate { get; set; } + public string Totp { get; set; } + public Fido2KeyApi Fido2Key { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Api/LoginUriApi.cs b/src/Maui/Bitwarden/Core/Models/Api/LoginUriApi.cs new file mode 100644 index 000000000..d9e59c65d --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Api/LoginUriApi.cs @@ -0,0 +1,10 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Models.Api +{ + public class LoginUriApi + { + public string Uri { get; set; } + public UriMatchType? Match { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Api/SecureNoteApi.cs b/src/Maui/Bitwarden/Core/Models/Api/SecureNoteApi.cs new file mode 100644 index 000000000..f311cfaa0 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Api/SecureNoteApi.cs @@ -0,0 +1,9 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Models.Api +{ + public class SecureNoteApi + { + public SecureNoteType Type { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Api/SendFileApi.cs b/src/Maui/Bitwarden/Core/Models/Api/SendFileApi.cs new file mode 100644 index 000000000..345d9f215 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Api/SendFileApi.cs @@ -0,0 +1,11 @@ +namespace Bit.Core.Models.Api +{ + public class SendFileApi + { + public string Id { get; set; } + public string FileName { get; set; } + public string Key { get; set; } + public string Size { get; set; } + public string SizeName { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Api/SendTextApi.cs b/src/Maui/Bitwarden/Core/Models/Api/SendTextApi.cs new file mode 100644 index 000000000..2b7b92429 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Api/SendTextApi.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Models.Api +{ + public class SendTextApi + { + public string Text { get; set; } + public bool Hidden { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/AttachmentData.cs b/src/Maui/Bitwarden/Core/Models/Data/AttachmentData.cs new file mode 100644 index 000000000..32fb585e4 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/AttachmentData.cs @@ -0,0 +1,26 @@ +using Bit.Core.Models.Response; + +namespace Bit.Core.Models.Data +{ + public class AttachmentData : Data + { + public AttachmentData() { } + + public AttachmentData(AttachmentResponse response) + { + Id = response.Id; + Url = response.Url; + FileName = response.FileName; + Key = response.Key; + Size = response.Size; + SizeName = response.SizeName; + } + + public string Id { get; set; } + public string Url { get; set; } + public string FileName { get; set; } + public string Key { get; set; } + public string Size { get; set; } + public string SizeName { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/CardData.cs b/src/Maui/Bitwarden/Core/Models/Data/CardData.cs new file mode 100644 index 000000000..43744f5c2 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/CardData.cs @@ -0,0 +1,26 @@ +using Bit.Core.Models.Api; + +namespace Bit.Core.Models.Data +{ + public class CardData : Data + { + public CardData() { } + + public CardData(CardApi data) + { + CardholderName = data.CardholderName; + Brand = data.Brand; + Number = data.Number; + ExpMonth = data.ExpMonth; + ExpYear = data.ExpYear; + Code = data.Code; + } + + public string CardholderName { get; set; } + public string Brand { get; set; } + public string Number { get; set; } + public string ExpMonth { get; set; } + public string ExpYear { get; set; } + public string Code { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/CipherData.cs b/src/Maui/Bitwarden/Core/Models/Data/CipherData.cs new file mode 100644 index 000000000..f8bd1a648 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/CipherData.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Bit.Core.Models.Response; + +namespace Bit.Core.Models.Data +{ + public class CipherData : Data + { + public CipherData() { } + + public CipherData(CipherResponse response, string userId = null, HashSet collectionIds = null) + { + Id = response.Id; + OrganizationId = response.OrganizationId; + FolderId = response.FolderId; + UserId = userId; + Edit = response.Edit; + ViewPassword = response.ViewPassword; + OrganizationUseTotp = response.OrganizationUseTotp; + Favorite = response.Favorite; + RevisionDate = response.RevisionDate; + CreationDate = response.CreationDate; + DeletedDate = response.DeletedDate; + Type = response.Type; + Name = response.Name; + Notes = response.Notes; + CollectionIds = collectionIds?.ToList() ?? response.CollectionIds; + Reprompt = response.Reprompt; + + try // Added to address Issue (https://github.com/bitwarden/mobile/issues/1006) + { + switch (Type) + { + case Enums.CipherType.Login: + Login = new LoginData(response.Login); + break; + case Enums.CipherType.SecureNote: + SecureNote = new SecureNoteData(response.SecureNote); + break; + case Enums.CipherType.Card: + Card = new CardData(response.Card); + break; + case Enums.CipherType.Identity: + Identity = new IdentityData(response.Identity); + break; + case Enums.CipherType.Fido2Key: + Fido2Key = new Fido2KeyData(response.Fido2Key); + break; + default: + break; + } + } + catch + { + System.Diagnostics.Trace.WriteLine(new StringBuilder() + .Append("BitWarden CipherData constructor failed to initialize CyperType '") + .Append(Type) + .Append("'; id = {") + .Append(Id) + .AppendLine("}") + .ToString(), "BitWarden CipherData constructor"); + } + + Fields = response.Fields?.Select(f => new FieldData(f)).ToList(); + Attachments = response.Attachments?.Select(a => new AttachmentData(a)).ToList(); + PasswordHistory = response.PasswordHistory?.Select(ph => new PasswordHistoryData(ph)).ToList(); + } + + public string Id { get; set; } + public string OrganizationId { get; set; } + public string FolderId { get; set; } + public string UserId { get; set; } + public bool Edit { get; set; } + public bool ViewPassword { get; set; } = true; // Fallback for old server versions + public bool OrganizationUseTotp { get; set; } + public bool Favorite { get; set; } + public DateTime RevisionDate { get; set; } + public DateTime CreationDate { get; set; } + public DateTime? DeletedDate { get; set; } + public Enums.CipherType Type { get; set; } + public string Name { get; set; } + public string Notes { get; set; } + public LoginData Login { get; set; } + public SecureNoteData SecureNote { get; set; } + public CardData Card { get; set; } + public IdentityData Identity { get; set; } + public Fido2KeyData Fido2Key { get; set; } + public List Fields { get; set; } + public List Attachments { get; set; } + public List PasswordHistory { get; set; } + public List CollectionIds { get; set; } + public Enums.CipherRepromptType Reprompt { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/CollectionData.cs b/src/Maui/Bitwarden/Core/Models/Data/CollectionData.cs new file mode 100644 index 000000000..67d70bfd9 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/CollectionData.cs @@ -0,0 +1,24 @@ +using Bit.Core.Models.Response; + +namespace Bit.Core.Models.Data +{ + public class CollectionData : Data + { + public CollectionData() { } + + public CollectionData(CollectionDetailsResponse response) + { + Id = response.Id; + OrganizationId = response.OrganizationId; + Name = response.Name; + ExternalId = response.ExternalId; + ReadOnly = response.ReadOnly; + } + + public string Id { get; set; } + public string OrganizationId { get; set; } + public string Name { get; set; } + public string ExternalId { get; set; } + public bool ReadOnly { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/Data.cs b/src/Maui/Bitwarden/Core/Models/Data/Data.cs new file mode 100644 index 000000000..2fae03c94 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/Data.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Bit.Core.Models.Data +{ + public abstract class Data + { + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/EnvironmentUrlData.cs b/src/Maui/Bitwarden/Core/Models/Data/EnvironmentUrlData.cs new file mode 100644 index 000000000..ae57f804b --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/EnvironmentUrlData.cs @@ -0,0 +1,24 @@ +namespace Bit.Core.Models.Data +{ + public class EnvironmentUrlData + { + public static EnvironmentUrlData DefaultUS = new EnvironmentUrlData { Base = "https://vault.bitwarden.com" }; + public static EnvironmentUrlData DefaultEU = new EnvironmentUrlData { Base = "https://vault.bitwarden.eu" }; + + public string Base { get; set; } + public string Api { get; set; } + public string Identity { get; set; } + public string Icons { get; set; } + public string Notifications { get; set; } + public string WebVault { get; set; } + public string Events { get; set; } + + public bool IsEmpty => string.IsNullOrEmpty(Base) + && string.IsNullOrEmpty(Api) + && string.IsNullOrEmpty(Identity) + && string.IsNullOrEmpty(Icons) + && string.IsNullOrEmpty(Notifications) + && string.IsNullOrEmpty(WebVault) + && string.IsNullOrEmpty(Events); + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/EventData.cs b/src/Maui/Bitwarden/Core/Models/Data/EventData.cs new file mode 100644 index 000000000..6a5007033 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/EventData.cs @@ -0,0 +1,12 @@ +using System; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Data +{ + public class EventData : Data + { + public EventType Type { get; set; } + public string CipherId { get; set; } + public DateTime Date { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/Fido2KeyData.cs b/src/Maui/Bitwarden/Core/Models/Data/Fido2KeyData.cs new file mode 100644 index 000000000..234406eee --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/Fido2KeyData.cs @@ -0,0 +1,34 @@ +using Bit.Core.Models.Api; + +namespace Bit.Core.Models.Data +{ + public class Fido2KeyData : Data + { + public Fido2KeyData() { } + + public Fido2KeyData(Fido2KeyApi apiData) + { + NonDiscoverableId = apiData.NonDiscoverableId; + KeyType = apiData.KeyType; + KeyAlgorithm = apiData.KeyAlgorithm; + KeyCurve = apiData.KeyCurve; + KeyValue = apiData.KeyValue; + RpId = apiData.RpId; + RpName = apiData.RpName; + UserHandle = apiData.UserHandle; + UserName = apiData.UserName; + Counter = apiData.Counter; + } + + public string NonDiscoverableId { get; set; } + public string KeyType { get; set; } = Constants.DefaultFido2KeyType; + public string KeyAlgorithm { get; set; } = Constants.DefaultFido2KeyAlgorithm; + public string KeyCurve { get; set; } = Constants.DefaultFido2KeyCurve; + public string KeyValue { get; set; } + public string RpId { get; set; } + public string RpName { get; set; } + public string UserHandle { get; set; } + public string UserName { get; set; } + public string Counter { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/FieldData.cs b/src/Maui/Bitwarden/Core/Models/Data/FieldData.cs new file mode 100644 index 000000000..bf898e156 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/FieldData.cs @@ -0,0 +1,23 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Api; + +namespace Bit.Core.Models.Data +{ + public class FieldData : Data + { + public FieldData() { } + + public FieldData(FieldApi data) + { + Type = data.Type; + Name = data.Name; + Value = data.Value; + LinkedId = data.LinkedId; + } + + public FieldType Type { get; set; } + public string Name { get; set; } + public string Value { get; set; } + public LinkedIdType? LinkedId { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/FolderData.cs b/src/Maui/Bitwarden/Core/Models/Data/FolderData.cs new file mode 100644 index 000000000..75e56d16e --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/FolderData.cs @@ -0,0 +1,23 @@ +using System; +using Bit.Core.Models.Response; + +namespace Bit.Core.Models.Data +{ + public class FolderData : Data + { + public FolderData() { } + + public FolderData(FolderResponse response, string userId) + { + UserId = userId; + Id = response.Id; + Name = response.Name; + RevisionDate = response.RevisionDate; + } + + public string Id { get; set; } + public string UserId { get; set; } + public string Name { get; set; } + public DateTime RevisionDate { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/IdentityData.cs b/src/Maui/Bitwarden/Core/Models/Data/IdentityData.cs new file mode 100644 index 000000000..ba98d8ae3 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/IdentityData.cs @@ -0,0 +1,50 @@ +using Bit.Core.Models.Api; + +namespace Bit.Core.Models.Data +{ + public class IdentityData : Data + { + public IdentityData() { } + + public IdentityData(IdentityApi data) + { + Title = data.Title; + FirstName = data.FirstName; + MiddleName = data.MiddleName; + LastName = data.LastName; + Address1 = data.Address1; + Address2 = data.Address2; + Address3 = data.Address3; + City = data.City; + State = data.State; + PostalCode = data.PostalCode; + Country = data.Country; + Company = data.Company; + Email = data.Email; + Phone = data.Phone; + SSN = data.SSN; + Username = data.Username; + PassportNumber = data.PassportNumber; + LicenseNumber = data.LicenseNumber; + } + + public string Title { get; set; } + public string FirstName { get; set; } + public string MiddleName { get; set; } + public string LastName { get; set; } + public string Address1 { get; set; } + public string Address2 { get; set; } + public string Address3 { get; set; } + public string City { get; set; } + public string State { get; set; } + public string PostalCode { get; set; } + public string Country { get; set; } + public string Company { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public string SSN { get; set; } + public string Username { get; set; } + public string PassportNumber { get; set; } + public string LicenseNumber { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/LoginData.cs b/src/Maui/Bitwarden/Core/Models/Data/LoginData.cs new file mode 100644 index 000000000..1460e0b17 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/LoginData.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Bit.Core.Models.Api; + +namespace Bit.Core.Models.Data +{ + public class LoginData : Data + { + public LoginData() { } + + public LoginData(LoginApi data) + { + Username = data.Username; + Password = data.Password; + PasswordRevisionDate = data.PasswordRevisionDate; + Totp = data.Totp; + Uris = data.Uris?.Select(u => new LoginUriData(u)).ToList(); + Fido2Key = data.Fido2Key != null ? new Fido2KeyData(data.Fido2Key) : null; + } + + public List Uris { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public DateTime? PasswordRevisionDate { get; set; } + public string Totp { get; set; } + public Fido2KeyData Fido2Key { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/LoginUriData.cs b/src/Maui/Bitwarden/Core/Models/Data/LoginUriData.cs new file mode 100644 index 000000000..52294c523 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/LoginUriData.cs @@ -0,0 +1,19 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Api; + +namespace Bit.Core.Models.Data +{ + public class LoginUriData : Data + { + public LoginUriData() { } + + public LoginUriData(LoginUriApi data) + { + Uri = data.Uri; + Match = data.Match; + } + + public string Uri { get; set; } + public UriMatchType? Match { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/OrganizationData.cs b/src/Maui/Bitwarden/Core/Models/Data/OrganizationData.cs new file mode 100644 index 000000000..e0d3be3fb --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/OrganizationData.cs @@ -0,0 +1,58 @@ +using System.Data.Common; +using Bit.Core.Enums; +using Bit.Core.Models.Response; + +namespace Bit.Core.Models.Data +{ + public class OrganizationData : Data + { + public OrganizationData() { } + + public OrganizationData(ProfileOrganizationResponse response) + { + Id = response.Id; + Name = response.Name; + Status = response.Status; + Type = response.Type; + Enabled = response.Enabled; + UseGroups = response.UseGroups; + UseDirectory = response.UseDirectory; + UseEvents = response.UseEvents; + UseTotp = response.UseTotp; + Use2fa = response.Use2fa; + UseApi = response.UseApi; + UsePolicies = response.UsePolicies; + SelfHost = response.SelfHost; + UsersGetPremium = response.UsersGetPremium; + Seats = response.Seats; + MaxCollections = response.MaxCollections; + MaxStorageGb = response.MaxStorageGb; + Permissions = response.Permissions ?? new Permissions(); + Identifier = response.Identifier; + UsesKeyConnector = response.UsesKeyConnector; + KeyConnectorUrl = response.KeyConnectorUrl; + } + + public string Id { get; set; } + public string Name { get; set; } + public OrganizationUserStatusType Status { get; set; } + public OrganizationUserType Type { get; set; } + public bool Enabled { get; set; } + public bool UseGroups { get; set; } + public bool UseDirectory { get; set; } + public bool UseEvents { get; set; } + public bool UseTotp { get; set; } + public bool Use2fa { get; set; } + public bool UseApi { get; set; } + public bool UsePolicies { get; set; } + public bool SelfHost { get; set; } + public bool UsersGetPremium { get; set; } + public int? Seats { get; set; } + public short? MaxCollections { get; set; } + public short? MaxStorageGb { get; set; } + public Permissions Permissions { get; set; } = new Permissions(); + public string Identifier { get; set; } + public bool UsesKeyConnector { get; set; } + public string KeyConnectorUrl { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/PasswordHistoryData.cs b/src/Maui/Bitwarden/Core/Models/Data/PasswordHistoryData.cs new file mode 100644 index 000000000..9ade26ef2 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/PasswordHistoryData.cs @@ -0,0 +1,19 @@ +using System; +using Bit.Core.Models.Response; + +namespace Bit.Core.Models.Data +{ + public class PasswordHistoryData : Data + { + public PasswordHistoryData() { } + + public PasswordHistoryData(PasswordHistoryResponse data) + { + Password = data.Password; + LastUsedDate = data.LastUsedDate; + } + + public string Password { get; set; } + public DateTime? LastUsedDate { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/Permissions.cs b/src/Maui/Bitwarden/Core/Models/Data/Permissions.cs new file mode 100644 index 000000000..c1b8278f2 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/Permissions.cs @@ -0,0 +1,19 @@ +namespace Bit.Core.Models.Data +{ + public class Permissions + { + public bool AccessBusinessPortal { get; set; } + public bool AccessEventLogs { get; set; } + public bool AccessImportExport { get; set; } + public bool AccessReports { get; set; } + public bool EditAssignedCollections { get; set; } + public bool DeleteAssignedCollections { get; set; } + public bool CreateNewCollections { get; set; } + public bool EditAnyCollection { get; set; } + public bool DeleteAnyCollection { get; set; } + public bool ManageGroups { get; set; } + public bool ManagePolicies { get; set; } + public bool ManageSso { get; set; } + public bool ManageUsers { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/PolicyData.cs b/src/Maui/Bitwarden/Core/Models/Data/PolicyData.cs new file mode 100644 index 000000000..bedee8a25 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/PolicyData.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Bit.Core.Enums; +using Bit.Core.Models.Response; + +namespace Bit.Core.Models.Data +{ + public class PolicyData : Data + { + public PolicyData() { } + + public PolicyData(PolicyResponse response) + { + Id = response.Id; + OrganizationId = response.OrganizationId; + Type = response.Type; + Data = response.Data; + Enabled = response.Enabled; + } + + public string Id { get; set; } + public string OrganizationId { get; set; } + public PolicyType Type { get; set; } + public Dictionary Data { get; set; } + public bool Enabled { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/PreviousPageInfo.cs b/src/Maui/Bitwarden/Core/Models/Data/PreviousPageInfo.cs new file mode 100644 index 000000000..bcf8e29c3 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/PreviousPageInfo.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Models.Data +{ + public class PreviousPageInfo + { + public string Page { get; set; } + public string CipherId { get; set; } + public string SendId { get; set; } + public string SearchText { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/SecureNoteData.cs b/src/Maui/Bitwarden/Core/Models/Data/SecureNoteData.cs new file mode 100644 index 000000000..8b89bab7b --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/SecureNoteData.cs @@ -0,0 +1,17 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Api; + +namespace Bit.Core.Models.Data +{ + public class SecureNoteData : Data + { + public SecureNoteData() { } + + public SecureNoteData(SecureNoteApi data) + { + Type = data.Type; + } + + public SecureNoteType Type { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/SendData.cs b/src/Maui/Bitwarden/Core/Models/Data/SendData.cs new file mode 100644 index 000000000..a4659c37f --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/SendData.cs @@ -0,0 +1,60 @@ +using System; +using Bit.Core.Enums; +using Bit.Core.Models.Response; + +namespace Bit.Core.Models.Data +{ + public class SendData : Data + { + public SendData() { } + + public SendData(SendResponse response, string userId) + { + Id = response.Id; + AccessId = response.AccessId; + UserId = userId; + Type = response.Type; + Name = response.Name; + Notes = response.Notes; + Key = response.Key; + MaxAccessCount = response.MaxAccessCount; + AccessCount = response.AccessCount; + RevisionDate = response.RevisionDate; + ExpirationDate = response.ExpirationDate; + DeletionDate = response.DeletionDate; + Password = response.Password; + Disabled = response.Disabled; + HideEmail = response.HideEmail.GetValueOrDefault(); + + switch (Type) + { + case SendType.File: + File = new SendFileData(response.File); + break; + case SendType.Text: + Text = new SendTextData(response.Text); + break; + default: + break; + } + } + + public string Id { get; set; } + public string AccessId { get; set; } + public string UserId { get; set; } + public SendType Type { get; set; } + public string Name { get; set; } + public string Notes { get; set; } + public SendFileData File { get; set; } + public SendTextData Text { get; set; } + public string Key { get; set; } + public int? MaxAccessCount { get; set; } + public int AccessCount { get; set; } + public DateTime RevisionDate { get; set; } + public DateTime? ExpirationDate { get; set; } + public DateTime DeletionDate { get; set; } + public string Password { get; set; } + public bool Disabled { get; set; } + public bool HideEmail { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/SendFileData.cs b/src/Maui/Bitwarden/Core/Models/Data/SendFileData.cs new file mode 100644 index 000000000..d596bde64 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/SendFileData.cs @@ -0,0 +1,25 @@ +using System.Drawing; +using Bit.Core.Models.Api; + +namespace Bit.Core.Models.Data +{ + public class SendFileData : Data + { + public SendFileData() { } + + public SendFileData(SendFileApi data) + { + Id = data.Id; + FileName = data.FileName; + Key = data.Key; + Size = data.Size; + SizeName = data.SizeName; + } + + public string Id { get; set; } + public string FileName { get; set; } + public string Key { get; set; } + public string Size { get; set; } + public string SizeName { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Data/SendTextData.cs b/src/Maui/Bitwarden/Core/Models/Data/SendTextData.cs new file mode 100644 index 000000000..ae4f4abfc --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Data/SendTextData.cs @@ -0,0 +1,19 @@ +using System.Drawing; +using Bit.Core.Models.Api; + +namespace Bit.Core.Models.Data +{ + public class SendTextData : Data + { + public SendTextData() { } + + public SendTextData(SendTextApi data) + { + Text = data.Text; + Hidden = data.Hidden; + } + + public string Text { get; set; } + public bool Hidden { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/Account.cs b/src/Maui/Bitwarden/Core/Models/Domain/Account.cs new file mode 100644 index 000000000..6267d5815 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/Account.cs @@ -0,0 +1,125 @@ +using System; +using Bit.Core.Enums; +using Bit.Core.Models.Data; + +namespace Bit.Core.Models.Domain +{ + public class Account : Domain + { + public AccountProfile Profile; + public AccountTokens Tokens; + public AccountSettings Settings; + public AccountVolatileData VolatileData; + + public Account() { } + + public Account(AccountProfile profile, AccountTokens tokens) + { + Profile = profile; + Tokens = tokens; + Settings = new AccountSettings(); + VolatileData = new AccountVolatileData(); + } + + public Account(Account account) + { + // Copy constructor excludes VolatileData (for storage) + Profile = new AccountProfile(account.Profile); + Tokens = new AccountTokens(account.Tokens); + Settings = new AccountSettings(account.Settings); + } + + public class AccountProfile + { + public AccountProfile() { } + + public AccountProfile(AccountProfile copy) + { + if (copy == null) + { + return; + } + + UserId = copy.UserId; + Email = copy.Email; + Name = copy.Name; + Stamp = copy.Stamp; + OrgIdentifier = copy.OrgIdentifier; + KdfType = copy.KdfType; + KdfIterations = copy.KdfIterations; + KdfMemory = copy.KdfMemory; + KdfParallelism = copy.KdfParallelism; + EmailVerified = copy.EmailVerified; + HasPremiumPersonally = copy.HasPremiumPersonally; + AvatarColor = copy.AvatarColor; + ForcePasswordResetReason = copy.ForcePasswordResetReason; + } + + public string UserId; + public string Email; + public string Name; + public string Stamp; + public string OrgIdentifier; + public string AvatarColor; + public KdfType? KdfType; + public int? KdfIterations; + public int? KdfMemory; + public int? KdfParallelism; + public bool? EmailVerified; + public bool? HasPremiumPersonally; + public ForcePasswordResetReason? ForcePasswordResetReason; + } + + public class AccountTokens + { + public AccountTokens() { } + + public AccountTokens(AccountTokens copy) + { + if (copy == null) + { + return; + } + + AccessToken = copy.AccessToken; + RefreshToken = copy.RefreshToken; + } + + public string AccessToken; + public string RefreshToken; + } + + public class AccountSettings + { + public AccountSettings() { } + + public AccountSettings(AccountSettings copy) + { + if (copy == null) + { + return; + } + + EnvironmentUrls = copy.EnvironmentUrls; + VaultTimeout = copy.VaultTimeout; + VaultTimeoutAction = copy.VaultTimeoutAction; + ScreenCaptureAllowed = copy.ScreenCaptureAllowed; + } + + public EnvironmentUrlData EnvironmentUrls; + [Obsolete("Feb 10 2023: VaultTimeout has been deprecated in favor of stored prefs to retain value after logout. It remains here to allow for migration during app upgrade.")] + public int? VaultTimeout; + [Obsolete("Feb 10 2023: VaultTimeoutAction has been deprecated in favor of stored prefs to retain value after logout. It remains here to allow for migration during app upgrade.")] + public VaultTimeoutAction? VaultTimeoutAction; + [Obsolete("Feb 10 2023: ScreenCaptureAllowed has been deprecated in favor of stored prefs to retain value after logout. It remains here to allow for migration during app upgrade.")] + public bool? ScreenCaptureAllowed; + } + + public class AccountVolatileData + { + public SymmetricCryptoKey Key; + public EncString PinProtectedKey; + public bool? BiometricLocked; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/Attachment.cs b/src/Maui/Bitwarden/Core/Models/Domain/Attachment.cs new file mode 100644 index 000000000..3e71d6107 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/Attachment.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; +using Bit.Core.Utilities; + +namespace Bit.Core.Models.Domain +{ + public class Attachment : Domain + { + private HashSet _map = new HashSet + { + "Id", + "Url", + "SizeName", + "FileName", + "Key" + }; + + public Attachment() { } + + public Attachment(AttachmentData obj, bool alreadyEncrypted = false) + { + Size = obj.Size; + BuildDomainModel(this, obj, _map, alreadyEncrypted, new HashSet { "Id", "Url", "SizeName" }); + } + + public string Id { get; set; } + public string Url { get; set; } + public string Size { get; set; } + public string SizeName { get; set; } + public EncString Key { get; set; } + public EncString FileName { get; set; } + + public async Task DecryptAsync(string orgId) + { + var view = await DecryptObjAsync(new AttachmentView(this), this, new HashSet + { + "FileName" + }, orgId); + + if (Key != null) + { + var cryptoService = ServiceContainer.Resolve("cryptoService"); + try + { + var orgKey = await cryptoService.GetOrgKeyAsync(orgId); + var decValue = await cryptoService.DecryptToBytesAsync(Key, orgKey); + view.Key = new SymmetricCryptoKey(decValue); + } + catch + { + // TODO: error? + } + } + return view; + } + + public AttachmentData ToAttachmentData() + { + var a = new AttachmentData(); + a.Size = Size; + BuildDataModel(this, a, _map, new HashSet { "Id", "Url", "SizeName" }); + return a; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/AuthResult.cs b/src/Maui/Bitwarden/Core/Models/Domain/AuthResult.cs new file mode 100644 index 000000000..8e48c7da7 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/AuthResult.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Domain +{ + public class AuthResult + { + public bool TwoFactor { get; set; } + public bool CaptchaNeeded => !string.IsNullOrWhiteSpace(CaptchaSiteKey); + public string CaptchaSiteKey { get; set; } + public bool ResetMasterPassword { get; set; } + public bool ForcePasswordReset { get; set; } + public Dictionary> TwoFactorProviders { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/Card.cs b/src/Maui/Bitwarden/Core/Models/Domain/Card.cs new file mode 100644 index 000000000..8a7ffff23 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/Card.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Domain +{ + public class Card : Domain + { + private HashSet _map = new HashSet + { + "CardholderName", + "Brand", + "Number", + "ExpMonth", + "ExpYear", + "Code" + }; + + public Card() { } + + public Card(CardData obj, bool alreadyEncrypted = false) + { + BuildDomainModel(this, obj, _map, alreadyEncrypted); + } + + public EncString CardholderName { get; set; } + public EncString Brand { get; set; } + public EncString Number { get; set; } + public EncString ExpMonth { get; set; } + public EncString ExpYear { get; set; } + public EncString Code { get; set; } + + public Task DecryptAsync(string orgId) + { + return DecryptObjAsync(new CardView(this), this, _map, orgId); + } + + public CardData ToCardData() + { + var c = new CardData(); + BuildDataModel(this, c, _map); + return c; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/Cipher.cs b/src/Maui/Bitwarden/Core/Models/Domain/Cipher.cs new file mode 100644 index 000000000..a5b6fcba4 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/Cipher.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Domain +{ + public class Cipher : Domain + { + public Cipher() { } + + public Cipher(CipherData obj, bool alreadyEncrypted = false, Dictionary localData = null) + { + BuildDomainModel(this, obj, new HashSet + { + "Id", + "OrganizationId", + "FolderId", + "Name", + "Notes" + }, alreadyEncrypted, new HashSet { "Id", "OrganizationId", "FolderId" }); + + Type = obj.Type; + Favorite = obj.Favorite; + OrganizationUseTotp = obj.OrganizationUseTotp; + Edit = obj.Edit; + ViewPassword = obj.ViewPassword; + RevisionDate = obj.RevisionDate; + CreationDate = obj.CreationDate; + CollectionIds = obj.CollectionIds != null ? new HashSet(obj.CollectionIds) : null; + LocalData = localData; + Reprompt = obj.Reprompt; + + switch (Type) + { + case Enums.CipherType.Login: + Login = new Login(obj.Login, alreadyEncrypted); + break; + case Enums.CipherType.SecureNote: + SecureNote = new SecureNote(obj.SecureNote, alreadyEncrypted); + break; + case Enums.CipherType.Card: + Card = new Card(obj.Card, alreadyEncrypted); + break; + case Enums.CipherType.Identity: + Identity = new Identity(obj.Identity, alreadyEncrypted); + break; + case CipherType.Fido2Key: + Fido2Key = new Fido2Key(obj.Fido2Key, alreadyEncrypted); + break; + default: + break; + } + + Attachments = obj.Attachments?.Select(a => new Attachment(a, alreadyEncrypted)).ToList(); + Fields = obj.Fields?.Select(f => new Field(f, alreadyEncrypted)).ToList(); + PasswordHistory = obj.PasswordHistory?.Select(ph => new PasswordHistory(ph, alreadyEncrypted)).ToList(); + DeletedDate = obj.DeletedDate; + } + + public string Id { get; set; } + public string OrganizationId { get; set; } + public string FolderId { get; set; } + public EncString Name { get; set; } + public EncString Notes { get; set; } + public Enums.CipherType Type { get; set; } + public bool Favorite { get; set; } + public bool OrganizationUseTotp { get; set; } + public bool Edit { get; set; } + public bool ViewPassword { get; set; } + public DateTime RevisionDate { get; set; } + public DateTime CreationDate { get; set; } + public DateTime? DeletedDate { get; set; } + public Dictionary LocalData { get; set; } + public Login Login { get; set; } + public Identity Identity { get; set; } + public Card Card { get; set; } + public SecureNote SecureNote { get; set; } + public Fido2Key Fido2Key { get; set; } + public List Attachments { get; set; } + public List Fields { get; set; } + public List PasswordHistory { get; set; } + public HashSet CollectionIds { get; set; } + public CipherRepromptType Reprompt { get; set; } + + public async Task DecryptAsync() + { + var model = new CipherView(this); + await DecryptObjAsync(model, this, new HashSet + { + "Name", + "Notes" + }, OrganizationId); + + switch (Type) + { + case Enums.CipherType.Login: + model.Login = await Login.DecryptAsync(OrganizationId); + break; + case Enums.CipherType.SecureNote: + model.SecureNote = await SecureNote.DecryptAsync(OrganizationId); + break; + case Enums.CipherType.Card: + model.Card = await Card.DecryptAsync(OrganizationId); + break; + case Enums.CipherType.Identity: + model.Identity = await Identity.DecryptAsync(OrganizationId); + break; + case Enums.CipherType.Fido2Key: + model.Fido2Key = await Fido2Key.DecryptAsync(OrganizationId); + break; + default: + break; + } + + if (Attachments?.Any() ?? false) + { + model.Attachments = new List(); + var tasks = new List(); + async Task decryptAndAddAttachmentAsync(Attachment attachment) + { + var decAttachment = await attachment.DecryptAsync(OrganizationId); + model.Attachments.Add(decAttachment); + } + foreach (var attachment in Attachments) + { + tasks.Add(decryptAndAddAttachmentAsync(attachment)); + } + await Task.WhenAll(tasks); + } + if (Fields?.Any() ?? false) + { + model.Fields = new List(); + var tasks = new List(); + async Task decryptAndAddFieldAsync(Field field) + { + var decField = await field.DecryptAsync(OrganizationId); + model.Fields.Add(decField); + } + foreach (var field in Fields) + { + tasks.Add(decryptAndAddFieldAsync(field)); + } + await Task.WhenAll(tasks); + } + if (PasswordHistory?.Any() ?? false) + { + model.PasswordHistory = new List(); + var tasks = new List(); + async Task decryptAndAddHistoryAsync(PasswordHistory ph) + { + var decPh = await ph.DecryptAsync(OrganizationId); + model.PasswordHistory.Add(decPh); + } + foreach (var ph in PasswordHistory) + { + tasks.Add(decryptAndAddHistoryAsync(ph)); + } + await Task.WhenAll(tasks); + } + return model; + } + + public CipherData ToCipherData(string userId) + { + var c = new CipherData + { + Id = Id, + OrganizationId = OrganizationId, + FolderId = FolderId, + UserId = OrganizationId != null ? userId : null, + Edit = Edit, + OrganizationUseTotp = OrganizationUseTotp, + Favorite = Favorite, + RevisionDate = RevisionDate, + CreationDate = CreationDate, + Type = Type, + CollectionIds = CollectionIds.ToList(), + DeletedDate = DeletedDate, + Reprompt = Reprompt, + }; + BuildDataModel(this, c, new HashSet + { + "Name", + "Notes" + }); + switch (c.Type) + { + case Enums.CipherType.Login: + c.Login = Login.ToLoginData(); + break; + case Enums.CipherType.SecureNote: + c.SecureNote = SecureNote.ToSecureNoteData(); + break; + case Enums.CipherType.Card: + c.Card = Card.ToCardData(); + break; + case Enums.CipherType.Identity: + c.Identity = Identity.ToIdentityData(); + break; + case Enums.CipherType.Fido2Key: + c.Fido2Key = Fido2Key.ToFido2KeyData(); + break; + default: + break; + } + c.Fields = Fields?.Select(f => f.ToFieldData()).ToList(); + c.Attachments = Attachments?.Select(a => a.ToAttachmentData()).ToList(); + c.PasswordHistory = PasswordHistory?.Select(ph => ph.ToPasswordHistoryData()).ToList(); + return c; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/Collection.cs b/src/Maui/Bitwarden/Core/Models/Domain/Collection.cs new file mode 100644 index 000000000..da62c55e2 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/Collection.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Domain +{ + public class Collection : Domain + { + public Collection() { } + + public Collection(CollectionData obj, bool alreadyEncrypted = false) + { + BuildDomainModel(this, obj, new HashSet + { + "Id", + "OrganizationId", + "Name", + "ExternalId", + "ReadOnly" + }, alreadyEncrypted, new HashSet + { + "Id", + "OrganizationId", + "ExternalId", + "ReadOnly" + }); + } + + public string Id { get; set; } + public string OrganizationId { get; set; } + public EncString Name { get; set; } + public string ExternalId { get; set; } + public bool ReadOnly { get; set; } + + public Task DecryptAsync() + { + return DecryptObjAsync(new CollectionView(this), this, new HashSet { "Name" }, OrganizationId); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/Domain.cs b/src/Maui/Bitwarden/Core/Models/Domain/Domain.cs new file mode 100644 index 000000000..493612a90 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/Domain.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.Core.Models.Domain +{ + public abstract class Domain + { + protected void BuildDomainModel(D domain, O dataObj, HashSet map, bool alreadyEncrypted, + HashSet notEncList = null) + where D : Domain + where O : Data.Data + { + var domainType = domain.GetType(); + var dataObjType = dataObj.GetType(); + foreach (var prop in map) + { + var dataObjPropInfo = dataObjType.GetProperty(prop); + var dataObjProp = dataObjPropInfo.GetValue(dataObj); + var domainPropInfo = domainType.GetProperty(prop); + if (alreadyEncrypted || (notEncList?.Contains(prop) ?? false)) + { + domainPropInfo.SetValue(domain, dataObjProp, null); + } + else + { + domainPropInfo.SetValue(domain, + dataObjProp != null ? new EncString(dataObjProp as string) : null, null); + } + } + } + + protected void BuildDataModel(D domain, O dataObj, HashSet map, + HashSet notEncryptedStringList = null) + where D : Domain + where O : Data.Data + { + var domainType = domain.GetType(); + var dataObjType = dataObj.GetType(); + foreach (var prop in map) + { + var domainPropInfo = domainType.GetProperty(prop); + var domainProp = domainPropInfo.GetValue(domain); + var dataObjPropInfo = dataObjType.GetProperty(prop); + if (notEncryptedStringList?.Contains(prop) ?? false) + { + dataObjPropInfo.SetValue(dataObj, domainProp, null); + } + else + { + dataObjPropInfo.SetValue(dataObj, (domainProp as EncString)?.EncryptedString, null); + } + } + } + + protected async Task DecryptObjAsync(V viewModel, D domain, HashSet map, string orgId, SymmetricCryptoKey key = null) + where V : View.View + { + var viewModelType = viewModel.GetType(); + var domainType = domain.GetType(); + + async Task decCsAndSetDec(string propName) + { + var domainPropInfo = domainType.GetProperty(propName); + string val = null; + if (domainPropInfo.GetValue(domain) is EncString domainProp) + { + val = await domainProp.DecryptAsync(orgId, key); + } + var viewModelPropInfo = viewModelType.GetProperty(propName); + viewModelPropInfo.SetValue(viewModel, val, null); + }; + + var tasks = new List(); + foreach (var prop in map) + { + tasks.Add(decCsAndSetDec(prop)); + } + await Task.WhenAll(tasks); + return viewModel; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/EncByteArray.cs b/src/Maui/Bitwarden/Core/Models/Domain/EncByteArray.cs new file mode 100644 index 000000000..97814a7aa --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/EncByteArray.cs @@ -0,0 +1,12 @@ +namespace Bit.Core.Models.Domain +{ + public class EncByteArray + { + public byte[] Buffer { get; } + + public EncByteArray(byte[] encryptedByteArray) + { + Buffer = encryptedByteArray; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/EncString.cs b/src/Maui/Bitwarden/Core/Models/Domain/EncString.cs new file mode 100644 index 000000000..2a86348a6 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/EncString.cs @@ -0,0 +1,125 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Utilities; + +namespace Bit.Core.Models.Domain +{ + public class EncString + { + private string _decryptedValue; + + public EncString(EncryptionType encryptionType, string data, string iv = null, string mac = null) + { + if (string.IsNullOrWhiteSpace(data)) + { + throw new ArgumentNullException(nameof(data)); + } + + if (!string.IsNullOrWhiteSpace(iv)) + { + EncryptedString = string.Format("{0}.{1}|{2}", (byte)encryptionType, iv, data); + } + else + { + EncryptedString = string.Format("{0}.{1}", (byte)encryptionType, data); + } + + if (!string.IsNullOrWhiteSpace(mac)) + { + EncryptedString = string.Format("{0}|{1}", EncryptedString, mac); + } + + EncryptionType = encryptionType; + Data = data; + Iv = iv; + Mac = mac; + } + + public EncString(string encryptedString) + { + if (string.IsNullOrWhiteSpace(encryptedString)) + { + throw new ArgumentException(nameof(encryptedString)); + } + + EncryptedString = encryptedString; + var headerPieces = EncryptedString.Split('.'); + string[] encPieces; + + if (headerPieces.Length == 2 && Enum.TryParse(headerPieces[0], out EncryptionType encType)) + { + EncryptionType = encType; + encPieces = headerPieces[1].Split('|'); + } + else + { + encPieces = EncryptedString.Split('|'); + EncryptionType = encPieces.Length == 3 ? EncryptionType.AesCbc128_HmacSha256_B64 : + EncryptionType.AesCbc256_B64; + } + + switch (EncryptionType) + { + case EncryptionType.AesCbc128_HmacSha256_B64: + case EncryptionType.AesCbc256_HmacSha256_B64: + if (encPieces.Length != 3) + { + return; + } + Iv = encPieces[0]; + Data = encPieces[1]; + Mac = encPieces[2]; + break; + case EncryptionType.AesCbc256_B64: + if (encPieces.Length != 2) + { + return; + } + Iv = encPieces[0]; + Data = encPieces[1]; + break; + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha1_B64: + if (encPieces.Length != 1) + { + return; + } + Data = encPieces[0]; + break; + default: + return; + } + } + + public EncryptionType EncryptionType { get; private set; } + public string EncryptedString { get; private set; } + public string Iv { get; private set; } + public string Data { get; private set; } + public string Mac { get; private set; } + + public async Task DecryptAsync(string orgId = null, SymmetricCryptoKey key = null) + { + if (_decryptedValue != null) + { + return _decryptedValue; + } + + var cryptoService = ServiceContainer.Resolve("cryptoService"); + try + { + if (key == null) + { + key = await cryptoService.GetOrgKeyAsync(orgId); + } + _decryptedValue = await cryptoService.DecryptToUtf8Async(this, key); + } + catch + { + _decryptedValue = "[error: cannot decrypt]"; + } + return _decryptedValue; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/EnvironmentUrls.cs b/src/Maui/Bitwarden/Core/Models/Domain/EnvironmentUrls.cs new file mode 100644 index 000000000..52cfff8d3 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/EnvironmentUrls.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Models.Domain +{ + public class EnvironmentUrls + { + public string Base { get; set; } + public string Api { get; set; } + public string Identity { get; set; } + public string Events { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/Fido2Key.cs b/src/Maui/Bitwarden/Core/Models/Domain/Fido2Key.cs new file mode 100644 index 000000000..05c5741dd --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/Fido2Key.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Domain +{ + public class Fido2Key : Domain + { + public static HashSet EncryptableProperties => new HashSet + { + nameof(NonDiscoverableId), + nameof(KeyType), + nameof(KeyAlgorithm), + nameof(KeyCurve), + nameof(KeyValue), + nameof(RpId), + nameof(RpName), + nameof(UserHandle), + nameof(UserName), + nameof(Counter) + }; + + public Fido2Key() { } + + public Fido2Key(Fido2KeyData data, bool alreadyEncrypted = false) + { + BuildDomainModel(this, data, EncryptableProperties, alreadyEncrypted); + } + + public EncString NonDiscoverableId { get; set; } + public EncString KeyType { get; set; } + public EncString KeyAlgorithm { get; set; } + public EncString KeyCurve { get; set; } + public EncString KeyValue { get; set; } + public EncString RpId { get; set; } + public EncString RpName { get; set; } + public EncString UserHandle { get; set; } + public EncString UserName { get; set; } + public EncString Counter { get; set; } + + public async Task DecryptAsync(string orgId) + { + return await DecryptObjAsync(new Fido2KeyView(), this, EncryptableProperties, orgId); + } + + public Fido2KeyData ToFido2KeyData() + { + var data = new Fido2KeyData(); + BuildDataModel(this, data, EncryptableProperties); + return data; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/Field.cs b/src/Maui/Bitwarden/Core/Models/Domain/Field.cs new file mode 100644 index 000000000..cbfdcbca3 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/Field.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Domain +{ + public class Field : Domain + { + private HashSet _map = new HashSet + { + "Name", + "Value" + }; + + public Field() { } + + public Field(FieldData obj, bool alreadyEncrypted = false) + { + Type = obj.Type; + LinkedId = obj.LinkedId; + BuildDomainModel(this, obj, _map, alreadyEncrypted); + } + + public EncString Name { get; set; } + public EncString Value { get; set; } + public FieldType Type { get; set; } + public LinkedIdType? LinkedId { get; set; } + + public Task DecryptAsync(string orgId) + { + return DecryptObjAsync(new FieldView(this), this, _map, orgId); + } + + public FieldData ToFieldData() + { + var f = new FieldData(); + BuildDataModel(this, f, new HashSet + { + "Name", + "Value", + "Type", + "LinkedId" + }, new HashSet + { + "Type", + "LinkedId" + }); + return f; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/Folder.cs b/src/Maui/Bitwarden/Core/Models/Domain/Folder.cs new file mode 100644 index 000000000..fe59ef5f0 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/Folder.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Domain +{ + public class Folder : Domain + { + public Folder() { } + + public Folder(FolderData obj, bool alreadyEncrypted = false) + { + BuildDomainModel(this, obj, new HashSet + { + "Id", + "Name" + }, alreadyEncrypted, new HashSet { "Id" }); + RevisionDate = obj.RevisionDate; + } + + public string Id { get; set; } + public EncString Name { get; set; } + public DateTime RevisionDate { get; set; } + + public Task DecryptAsync() + { + return DecryptObjAsync(new FolderView(this), this, new HashSet { "Name" }, null); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/ForcePasswordResetReason.cs b/src/Maui/Bitwarden/Core/Models/Domain/ForcePasswordResetReason.cs new file mode 100644 index 000000000..70b701bdd --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/ForcePasswordResetReason.cs @@ -0,0 +1,16 @@ +namespace Bit.Core.Models.Domain +{ + public enum ForcePasswordResetReason + { + /// + /// Occurs when an organization admin forces a user to reset their password. + /// + AdminForcePasswordReset, + + /// + /// Occurs when a user logs in with a master password that does not meet an organization's master password + /// policy that is enforced on login. + /// + WeakMasterPasswordOnLogin + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/GeneratedPasswordHistory.cs b/src/Maui/Bitwarden/Core/Models/Domain/GeneratedPasswordHistory.cs new file mode 100644 index 000000000..d4c176dac --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/GeneratedPasswordHistory.cs @@ -0,0 +1,10 @@ +using System; + +namespace Bit.Core.Models.Domain +{ + public class GeneratedPasswordHistory + { + public string Password { get; set; } + public DateTime Date { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/ITreeNodeObject.cs b/src/Maui/Bitwarden/Core/Models/Domain/ITreeNodeObject.cs new file mode 100644 index 000000000..f91c89151 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/ITreeNodeObject.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Models.Domain +{ + public interface ITreeNodeObject + { + string Id { get; set; } + string Name { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/Identity.cs b/src/Maui/Bitwarden/Core/Models/Domain/Identity.cs new file mode 100644 index 000000000..4a705dcb6 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/Identity.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Domain +{ + public class Identity : Domain + { + private HashSet _map = new HashSet + { + "Title", + "FirstName", + "MiddleName", + "LastName", + "Address1", + "Address2", + "Address3", + "City", + "State", + "PostalCode", + "Country", + "Company", + "Email", + "Phone", + "SSN", + "Username", + "PassportNumber", + "LicenseNumber" + }; + + public Identity() { } + + public Identity(IdentityData obj, bool alreadyEncrypted = false) + { + BuildDomainModel(this, obj, _map, alreadyEncrypted); + } + + public EncString Title { get; set; } + public EncString FirstName { get; set; } + public EncString MiddleName { get; set; } + public EncString LastName { get; set; } + public EncString Address1 { get; set; } + public EncString Address2 { get; set; } + public EncString Address3 { get; set; } + public EncString City { get; set; } + public EncString State { get; set; } + public EncString PostalCode { get; set; } + public EncString Country { get; set; } + public EncString Company { get; set; } + public EncString Email { get; set; } + public EncString Phone { get; set; } + public EncString SSN { get; set; } + public EncString Username { get; set; } + public EncString PassportNumber { get; set; } + public EncString LicenseNumber { get; set; } + + public Task DecryptAsync(string orgId) + { + return DecryptObjAsync(new IdentityView(this), this, _map, orgId); + } + + public IdentityData ToIdentityData() + { + var i = new IdentityData(); + BuildDataModel(this, i, _map); + return i; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/KdfConfiguration.cs b/src/Maui/Bitwarden/Core/Models/Domain/KdfConfiguration.cs new file mode 100755 index 000000000..c185565be --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/KdfConfiguration.cs @@ -0,0 +1,27 @@ +using Bit.Core; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +public struct KdfConfig +{ + public static KdfConfig Default = new KdfConfig(KdfType.PBKDF2_SHA256, 5000, null, null); + public KdfConfig(KdfType? type, int? iterations, int? memory, int? parallelism) + { + Type = type; + Iterations = iterations; + Memory = memory; + Parallelism = parallelism; + } + + public KdfConfig(Account.AccountProfile profile) + { + Type = profile.KdfType; + Iterations = profile.KdfIterations; + Memory = profile.KdfMemory; + Parallelism = profile.KdfParallelism; + } + + public KdfType? Type { get; set; } + public int? Iterations { get; set; } + public int? Memory { get; set; } + public int? Parallelism { get; set; } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/Login.cs b/src/Maui/Bitwarden/Core/Models/Domain/Login.cs new file mode 100644 index 000000000..9968f43a6 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/Login.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Domain +{ + public class Login : Domain + { + public Login() { } + + public Login(LoginData obj, bool alreadyEncrypted = false) + { + PasswordRevisionDate = obj.PasswordRevisionDate; + Uris = obj.Uris?.Select(u => new LoginUri(u, alreadyEncrypted)).ToList(); + Fido2Key = obj.Fido2Key != null ? new Fido2Key(obj.Fido2Key, alreadyEncrypted) : null; + BuildDomainModel(this, obj, new HashSet + { + "Username", + "Password", + "Totp" + }, alreadyEncrypted); + } + + public List Uris { get; set; } + public EncString Username { get; set; } + public EncString Password { get; set; } + public DateTime? PasswordRevisionDate { get; set; } + public EncString Totp { get; set; } + public Fido2Key Fido2Key { get; set; } + + public async Task DecryptAsync(string orgId) + { + var view = await DecryptObjAsync(new LoginView(this), this, new HashSet + { + "Username", + "Password", + "Totp" + }, orgId); + if (Uris != null) + { + view.Uris = new List(); + foreach (var uri in Uris) + { + view.Uris.Add(await uri.DecryptAsync(orgId)); + } + } + if (Fido2Key != null) + { + view.Fido2Key = await Fido2Key.DecryptAsync(orgId); + } + return view; + } + + public LoginData ToLoginData() + { + var l = new LoginData(); + l.PasswordRevisionDate = PasswordRevisionDate; + BuildDataModel(this, l, new HashSet + { + "Username", + "Password", + "Totp" + }); + if (Uris?.Any() ?? false) + { + l.Uris = Uris.Select(u => u.ToLoginUriData()).ToList(); + } + if (Fido2Key != null) + { + l.Fido2Key = Fido2Key.ToFido2KeyData(); + } + return l; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/LoginUri.cs b/src/Maui/Bitwarden/Core/Models/Domain/LoginUri.cs new file mode 100644 index 000000000..31747fd96 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/LoginUri.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Domain +{ + public class LoginUri : Domain + { + private HashSet _map = new HashSet + { + "Uri" + }; + + public LoginUri() { } + + public LoginUri(LoginUriData obj, bool alreadyEncrypted = false) + { + Match = obj.Match; + BuildDomainModel(this, obj, _map, alreadyEncrypted); + } + + public EncString Uri { get; set; } + public UriMatchType? Match { get; set; } + + public Task DecryptAsync(string orgId) + { + return DecryptObjAsync(new LoginUriView(this), this, _map, orgId); + } + + public LoginUriData ToLoginUriData() + { + var u = new LoginUriData(); + BuildDataModel(this, u, _map, new HashSet { "Match" }); + return u; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/MasterPasswordPolicyOptions.cs b/src/Maui/Bitwarden/Core/Models/Domain/MasterPasswordPolicyOptions.cs new file mode 100644 index 000000000..974791b52 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/MasterPasswordPolicyOptions.cs @@ -0,0 +1,23 @@ +namespace Bit.Core.Models.Domain +{ + public class MasterPasswordPolicyOptions + { + public int MinComplexity { get; set; } + public int MinLength { get; set; } + public bool RequireUpper { get; set; } + public bool RequireLower { get; set; } + public bool RequireNumbers { get; set; } + public bool RequireSpecial { get; set; } + public bool EnforceOnLogin { get; set; } + + public bool InEffect() + { + return MinComplexity > 0 || + MinLength > 0 || + RequireUpper || + RequireLower || + RequireNumbers || + RequireSpecial; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/Message.cs b/src/Maui/Bitwarden/Core/Models/Domain/Message.cs new file mode 100644 index 000000000..1553cbf72 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/Message.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Models.Domain +{ + public class Message + { + public string Command { get; set; } + public object Data { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/Organization.cs b/src/Maui/Bitwarden/Core/Models/Domain/Organization.cs new file mode 100644 index 000000000..a483fd3ad --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/Organization.cs @@ -0,0 +1,103 @@ +using System.Data.Common; +using Bit.Core.Enums; +using Bit.Core.Models.Data; + +namespace Bit.Core.Models.Domain +{ + public class Organization + { + public Organization() { } + + public Organization(OrganizationData obj) + { + Id = obj.Id; + Name = obj.Name; + Status = obj.Status; + Type = obj.Type; + Enabled = obj.Enabled; + UseGroups = obj.UseGroups; + UseDirectory = obj.UseDirectory; + UseEvents = obj.UseEvents; + UseTotp = obj.UseTotp; + Use2fa = obj.Use2fa; + UseApi = obj.UseApi; + UsePolicies = obj.UsePolicies; + SelfHost = obj.SelfHost; + UsersGetPremium = obj.UsersGetPremium; + Seats = obj.Seats; + MaxCollections = obj.MaxCollections; + MaxStorageGb = obj.MaxStorageGb; + Permissions = obj.Permissions ?? new Permissions(); + Identifier = obj.Identifier; + UsesKeyConnector = obj.UsesKeyConnector; + KeyConnectorUrl = obj.KeyConnectorUrl; + } + + public string Id { get; set; } + public string Name { get; set; } + public OrganizationUserStatusType Status { get; set; } + public OrganizationUserType Type { get; set; } + public bool Enabled { get; set; } + public bool UseGroups { get; set; } + public bool UseDirectory { get; set; } + public bool UseEvents { get; set; } + public bool UseTotp { get; set; } + public bool Use2fa { get; set; } + public bool UseApi { get; set; } + public bool UsePolicies { get; set; } + public bool SelfHost { get; set; } + public bool UsersGetPremium { get; set; } + public int? Seats { get; set; } + public short? MaxCollections { get; set; } + public short? MaxStorageGb { get; set; } + public Permissions Permissions { get; set; } = new Permissions(); + public string Identifier { get; set; } + public bool UsesKeyConnector { get; set; } + public string KeyConnectorUrl { get; set; } + + public bool CanAccess + { + get + { + if (Type == OrganizationUserType.Owner) + { + return true; + } + return Enabled && Status == OrganizationUserStatusType.Confirmed; + } + } + + public bool IsManager + { + get + { + switch (Type) + { + case OrganizationUserType.Owner: + case OrganizationUserType.Admin: + case OrganizationUserType.Manager: + return true; + default: + return false; + } + } + } + + public bool IsAdmin => Type == OrganizationUserType.Owner || Type == OrganizationUserType.Admin; + public bool IsOwner => Type == OrganizationUserType.Owner; + public bool IsCustom => Type == OrganizationUserType.Custom; + public bool canAccessBusinessPortl => IsAdmin || Permissions.AccessBusinessPortal; + public bool canAccessEventLogs => IsAdmin || Permissions.AccessEventLogs; + public bool canAccessImportExport => IsAdmin || Permissions.AccessImportExport; + public bool canAccessReports => IsAdmin || Permissions.AccessReports; + public bool canCreateNewCollections => IsAdmin || Permissions.CreateNewCollections; + public bool canEditAnyCollection => IsAdmin || Permissions.EditAnyCollection; + public bool canDeleteAnyCollection => IsAdmin || Permissions.DeleteAnyCollection; + public bool canEditAssignedCollections => IsManager || Permissions.EditAssignedCollections; + public bool canDeleteAssignedCollections => IsManager || Permissions.DeleteAssignedCollections; + public bool canManageGroups => IsAdmin || Permissions.ManageGroups; + public bool canManagePolicies => IsAdmin || Permissions.ManagePolicies; + public bool canManageUser => IsAdmin || Permissions.ManageUsers; + public bool isExemptFromPolicies => canManagePolicies; + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/PasswordGenerationOptions.cs b/src/Maui/Bitwarden/Core/Models/Domain/PasswordGenerationOptions.cs new file mode 100644 index 000000000..bab849661 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/PasswordGenerationOptions.cs @@ -0,0 +1,140 @@ +namespace Bit.Core.Models.Domain +{ + public class PasswordGenerationOptions + { + public const string TYPE_PASSWORD = "password"; + public const string TYPE_PASSPHRASE = "passphrase"; + + public static PasswordGenerationOptions CreateDefault => new PasswordGenerationOptions + { + Length = 14, + AllowAmbiguousChar = true, + Number = true, + MinNumber = 1, + Uppercase = true, + MinUppercase = 0, + Lowercase = true, + MinLowercase = 0, + Special = false, + MinSpecial = 1, + Type = TYPE_PASSWORD, + NumWords = 3, + WordSeparator = "-", + Capitalize = false, + IncludeNumber = false + }; + + public PasswordGenerationOptions() { } + + public int? Length { get; set; } + public bool? AllowAmbiguousChar { get; set; } + public bool? Number { get; set; } + public int? MinNumber { get; set; } + public bool? Uppercase { get; set; } + public int? MinUppercase { get; set; } + public bool? Lowercase { get; set; } + public int? MinLowercase { get; set; } + public bool? Special { get; set; } + public int? MinSpecial { get; set; } + public string Type { get; set; } + public int? NumWords { get; set; } + public string WordSeparator { get; set; } + public bool? Capitalize { get; set; } + public bool? IncludeNumber { get; set; } + + public PasswordGenerationOptions WithLength(int? length) + { + Length = length; + return this; + } + + public void Merge(PasswordGenerationOptions defaults) + { + Length = Length ?? defaults.Length; + AllowAmbiguousChar = AllowAmbiguousChar ?? defaults.AllowAmbiguousChar; + Number = Number ?? defaults.Number; + MinNumber = MinNumber ?? defaults.MinNumber; + Uppercase = Uppercase ?? defaults.Uppercase; + MinUppercase = MinUppercase ?? defaults.MinUppercase; + Lowercase = Lowercase ?? defaults.Lowercase; + MinLowercase = MinLowercase ?? defaults.MinLowercase; + Special = Special ?? defaults.Special; + MinSpecial = MinSpecial ?? defaults.MinSpecial; + Type = Type ?? defaults.Type; + NumWords = NumWords ?? defaults.NumWords; + WordSeparator = WordSeparator ?? defaults.WordSeparator; + Capitalize = Capitalize ?? defaults.Capitalize; + IncludeNumber = IncludeNumber ?? defaults.IncludeNumber; + } + + public void EnforcePolicy(PasswordGeneratorPolicyOptions enforcedPolicyOptions) + { + if (enforcedPolicyOptions is null) + { + return; + } + + if (Length < enforcedPolicyOptions.MinLength) + { + Length = enforcedPolicyOptions.MinLength; + } + + if (enforcedPolicyOptions.UseUppercase) + { + Uppercase = true; + } + + if (enforcedPolicyOptions.UseLowercase) + { + Lowercase = true; + } + + if (enforcedPolicyOptions.UseNumbers) + { + Number = true; + } + + if (MinNumber < enforcedPolicyOptions.NumberCount) + { + MinNumber = enforcedPolicyOptions.NumberCount; + } + + if (enforcedPolicyOptions.UseSpecial) + { + Special = true; + } + + if (MinSpecial < enforcedPolicyOptions.SpecialCount) + { + MinSpecial = enforcedPolicyOptions.SpecialCount; + } + + // Must normalize these fields because the receiving call expects all options to pass the current rules + if (MinSpecial + MinNumber > Length) + { + MinSpecial = Length - MinNumber; + } + + if (NumWords < enforcedPolicyOptions.MinNumberOfWords) + { + NumWords = enforcedPolicyOptions.MinNumberOfWords; + } + + if (enforcedPolicyOptions.Capitalize) + { + Capitalize = true; + } + + if (enforcedPolicyOptions.IncludeNumber) + { + IncludeNumber = true; + } + + // Force default type if password/passphrase selected via policy + if (enforcedPolicyOptions.DefaultType == TYPE_PASSWORD || enforcedPolicyOptions.DefaultType == TYPE_PASSPHRASE) + { + Type = enforcedPolicyOptions.DefaultType; + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/PasswordGeneratorPolicyOptions.cs b/src/Maui/Bitwarden/Core/Models/Domain/PasswordGeneratorPolicyOptions.cs new file mode 100644 index 000000000..9d6e0d8b5 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/PasswordGeneratorPolicyOptions.cs @@ -0,0 +1,32 @@ +namespace Bit.Core.Models.Domain +{ + public class PasswordGeneratorPolicyOptions + { + public string DefaultType { get; set; } = string.Empty; + public int MinLength { get; set; } + public bool UseUppercase { get; set; } + public bool UseLowercase { get; set; } + public bool UseNumbers { get; set; } + public int NumberCount { get; set; } + public bool UseSpecial { get; set; } + public int SpecialCount { get; set; } + public int MinNumberOfWords { get; set; } + public bool Capitalize { get; set; } + public bool IncludeNumber { get; set; } + + public bool InEffect() + { + return DefaultType != string.Empty || + MinLength > 0 || + NumberCount > 0 || + SpecialCount > 0 || + UseUppercase || + UseLowercase || + UseNumbers || + UseSpecial || + MinNumberOfWords > 0 || + Capitalize || + IncludeNumber; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/PasswordHistory.cs b/src/Maui/Bitwarden/Core/Models/Domain/PasswordHistory.cs new file mode 100644 index 000000000..6ae1ab1f1 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/PasswordHistory.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Domain +{ + public class PasswordHistory : Domain + { + private HashSet _map = new HashSet + { + "Password" + }; + + public PasswordHistory() { } + + public PasswordHistory(PasswordHistoryData obj, bool alreadyEncrypted = false) + { + BuildDomainModel(this, obj, _map, alreadyEncrypted); + LastUsedDate = obj.LastUsedDate.GetValueOrDefault(); + } + + public EncString Password { get; set; } + public DateTime LastUsedDate { get; set; } + + public Task DecryptAsync(string orgId) + { + return DecryptObjAsync(new PasswordHistoryView(this), this, _map, orgId); + } + + public PasswordHistoryData ToPasswordHistoryData() + { + var ph = new PasswordHistoryData(); + ph.LastUsedDate = LastUsedDate; + BuildDataModel(this, ph, _map); + return ph; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/Policy.cs b/src/Maui/Bitwarden/Core/Models/Domain/Policy.cs new file mode 100644 index 000000000..3d58c2fb8 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/Policy.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using Bit.Core.Enums; +using Bit.Core.Models.Data; + +namespace Bit.Core.Models.Domain +{ + public class Policy : Domain + { + public const string MINUTES_KEY = "minutes"; + public const string ACTION_KEY = "action"; + public const string ACTION_LOCK = "lock"; + public const string ACTION_LOGOUT = "logOut"; + + public Policy() { } + + public Policy(PolicyData obj) + { + Id = obj.Id; + OrganizationId = obj.OrganizationId; + Type = obj.Type; + Data = obj.Data; + Enabled = obj.Enabled; + } + + public string Id { get; set; } + public string OrganizationId { get; set; } + public PolicyType Type { get; set; } + public Dictionary Data { get; set; } + public bool Enabled { get; set; } + + public int? GetInt(string key) + { + if (Data.TryGetValue(key, out var val) && val != null) + { + return (int)(long)val; + } + return null; + } + + public bool? GetBool(string key) + { + if (Data.TryGetValue(key, out var val) && val != null) + { + return (bool)val; + } + return null; + } + + public string GetString(string key) + { + if (Data.TryGetValue(key, out var val)) + { + return (string)val; + } + return null; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/ResetPasswordPolicyOptions.cs b/src/Maui/Bitwarden/Core/Models/Domain/ResetPasswordPolicyOptions.cs new file mode 100644 index 000000000..4e9cebe2e --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/ResetPasswordPolicyOptions.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Domain +{ + public class ResetPasswordPolicyOptions + { + public bool AutoEnrollEnabled { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/SecureNote.cs b/src/Maui/Bitwarden/Core/Models/Domain/SecureNote.cs new file mode 100644 index 000000000..55817ab15 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/SecureNote.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Domain +{ + public class SecureNote : Domain + { + public SecureNote() { } + + public SecureNote(SecureNoteData obj, bool alreadyEncrypted = false) + { + Type = obj.Type; + } + + public SecureNoteType Type { get; set; } + + public Task DecryptAsync(string orgId) + { + return Task.FromResult(new SecureNoteView(this)); + } + + public SecureNoteData ToSecureNoteData() + { + return new SecureNoteData + { + Type = Type + }; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/Send.cs b/src/Maui/Bitwarden/Core/Models/Domain/Send.cs new file mode 100644 index 000000000..4ecea0c15 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/Send.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; +using Bit.Core.Utilities; + +namespace Bit.Core.Models.Domain +{ + public class Send : Domain + { + public string Id { get; set; } + public string AccessId { get; set; } + public string UserId { get; set; } + public SendType Type { get; set; } + public EncString Name { get; set; } + public EncString Notes { get; set; } + public SendFile File { get; set; } + public SendText Text { get; set; } + public EncString Key { get; set; } + public int? MaxAccessCount { get; set; } + public int AccessCount { get; set; } + public DateTime RevisionDate { get; set; } + public DateTime? ExpirationDate { get; set; } + public DateTime DeletionDate { get; set; } + public string Password { get; set; } + public bool Disabled { get; set; } + public bool HideEmail { get; set; } + + public Send() : base() { } + + public Send(SendData data, bool alreadyEncrypted = false) : base() + { + BuildDomainModel(this, data, new HashSet{ + "Id", + "AccessId", + "UserId", + "Name", + "Notes", + "Key", + }, alreadyEncrypted, new HashSet { "Id", "AccessId", "UserId" }); + + Type = data.Type; + MaxAccessCount = data.MaxAccessCount; + AccessCount = data.AccessCount; + Password = data.Password; + Disabled = data.Disabled; + RevisionDate = data.RevisionDate; + DeletionDate = data.DeletionDate; + ExpirationDate = data.ExpirationDate; + HideEmail = data.HideEmail; + + switch (Type) + { + case SendType.Text: + Text = new SendText(data.Text, alreadyEncrypted); + break; + case SendType.File: + File = new SendFile(data.File, alreadyEncrypted); + break; + default: + break; + } + } + + public async Task DecryptAsync() + { + var view = new SendView(this); + + var cryptoService = ServiceContainer.Resolve("cryptoService"); + + view.Key = await cryptoService.DecryptToBytesAsync(Key, null); + view.CryptoKey = await cryptoService.MakeSendKeyAsync(view.Key); + + await DecryptObjAsync(view, this, new HashSet { "Name", "Notes" }, null, view.CryptoKey); + + switch (Type) + { + case SendType.File: + view.File = await this.File.DecryptAsync(view.CryptoKey); + break; + case SendType.Text: + view.Text = await this.Text.DecryptAsync(view.CryptoKey); + break; + default: + break; + } + return view; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/SendFile.cs b/src/Maui/Bitwarden/Core/Models/Domain/SendFile.cs new file mode 100644 index 000000000..cde9086d1 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/SendFile.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Domain +{ + public class SendFile : Domain + { + public string Id { get; set; } + public string Size { get; set; } + public string SizeName { get; set; } + public EncString FileName { get; set; } + + public SendFile() : base() { } + + public SendFile(SendFileData file, bool alreadyEncrypted = false) : base() + { + Size = file.Size; + BuildDomainModel(this, file, new HashSet { "Id", "SizeName", "FileName" }, alreadyEncrypted, new HashSet { "Id", "SizeName" }); + } + + public Task DecryptAsync(SymmetricCryptoKey key) => + DecryptObjAsync(new SendFileView(this), this, new HashSet { "FileName" }, null, key); + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/SendText.cs b/src/Maui/Bitwarden/Core/Models/Domain/SendText.cs new file mode 100644 index 000000000..adff8251f --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/SendText.cs @@ -0,0 +1,25 @@ + +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Domain +{ + public class SendText : Domain + { + public EncString Text { get; set; } + public bool Hidden { get; set; } + + public SendText() : base() { } + + public SendText(SendTextData data, bool alreadyEncrypted = false) : base() + { + Hidden = data.Hidden; + BuildDomainModel(this, data, new HashSet { "Text" }, alreadyEncrypted); + } + + public Task DecryptAsync(SymmetricCryptoKey key) => + DecryptObjAsync(new SendTextView(this), this, new HashSet { "Text" }, null, key); + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/State.cs b/src/Maui/Bitwarden/Core/Models/Domain/State.cs new file mode 100644 index 000000000..f8180fab8 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/State.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Domain +{ + public class State : Domain + { + public Dictionary Accounts { get; set; } + public string ActiveUserId { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/StorageOptions.cs b/src/Maui/Bitwarden/Core/Models/Domain/StorageOptions.cs new file mode 100644 index 000000000..0d9061876 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/StorageOptions.cs @@ -0,0 +1,13 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Models.Domain +{ + public class StorageOptions : Domain + { + public StorageLocation? StorageLocation { get; set; } + public bool? UseSecureStorage { get; set; } + public string UserId { get; set; } + public string Email { get; set; } + public bool? SkipTokenStorage { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/SymmetricCryptoKey.cs b/src/Maui/Bitwarden/Core/Models/Domain/SymmetricCryptoKey.cs new file mode 100644 index 000000000..91bce7550 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/SymmetricCryptoKey.cs @@ -0,0 +1,77 @@ +using System; +using System.Linq; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Domain +{ + public class SymmetricCryptoKey + { + public SymmetricCryptoKey(byte[] key, EncryptionType? encType = null) + { + if (key == null) + { + throw new Exception("Must provide key."); + } + + if (encType == null) + { + if (key.Length == 32) + { + encType = EncryptionType.AesCbc256_B64; + } + else if (key.Length == 64) + { + encType = EncryptionType.AesCbc256_HmacSha256_B64; + } + else + { + throw new Exception("Unable to determine encType."); + } + } + + Key = key; + EncType = encType.Value; + + if (EncType == EncryptionType.AesCbc256_B64 && Key.Length == 32) + { + EncKey = Key; + MacKey = null; + } + else if (EncType == EncryptionType.AesCbc128_HmacSha256_B64 && Key.Length == 32) + { + EncKey = new ArraySegment(Key, 0, 16).ToArray(); + MacKey = new ArraySegment(Key, 16, 16).ToArray(); + } + else if (EncType == EncryptionType.AesCbc256_HmacSha256_B64 && Key.Length == 64) + { + EncKey = new ArraySegment(Key, 0, 32).ToArray(); + MacKey = new ArraySegment(Key, 32, 32).ToArray(); + } + else + { + throw new Exception("Unsupported encType/key length."); + } + + if (Key != null) + { + KeyB64 = Convert.ToBase64String(Key); + } + if (EncKey != null) + { + EncKeyB64 = Convert.ToBase64String(EncKey); + } + if (MacKey != null) + { + MacKeyB64 = Convert.ToBase64String(MacKey); + } + } + + public byte[] Key { get; set; } + public byte[] EncKey { get; set; } + public byte[] MacKey { get; set; } + public EncryptionType EncType { get; set; } + public string KeyB64 { get; set; } + public string EncKeyB64 { get; set; } + public string MacKeyB64 { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/TreeNode.cs b/src/Maui/Bitwarden/Core/Models/Domain/TreeNode.cs new file mode 100644 index 000000000..524b81e63 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/TreeNode.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Domain +{ + public class TreeNode where T : ITreeNodeObject + { + public T Parent { get; set; } + public T Node { get; set; } + public List> Children { get; set; } = new List>(); + + public TreeNode(T node, string name, T parent) + { + Parent = parent; + Node = node; + Node.Name = name; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/TwoFactorProvider.cs b/src/Maui/Bitwarden/Core/Models/Domain/TwoFactorProvider.cs new file mode 100644 index 000000000..be3489a08 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/TwoFactorProvider.cs @@ -0,0 +1,14 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Models.Domain +{ + public class TwoFactorProvider + { + public TwoFactorProviderType Type { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public int Priority { get; set; } + public int Sort { get; set; } + public bool Premium { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Domain/UsernameGenerationOptions.cs b/src/Maui/Bitwarden/Core/Models/Domain/UsernameGenerationOptions.cs new file mode 100644 index 000000000..deb98521d --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Domain/UsernameGenerationOptions.cs @@ -0,0 +1,57 @@ +using Bit.Core.Enums; +using Bit.Core.Services.EmailForwarders; + +namespace Bit.Core.Models.Domain +{ + public class UsernameGenerationOptions + { + public UsernameGenerationOptions() + { + ServiceType = ForwardedEmailServiceType.None; + } + + public UsernameType Type { get; set; } + public ForwardedEmailServiceType ServiceType { get; set; } + public UsernameEmailType PlusAddressedEmailType { get; set; } + public UsernameEmailType CatchAllEmailType { get; set; } + public bool CapitalizeRandomWordUsername { get; set; } + public bool IncludeNumberRandomWordUsername { get; set; } + public string PlusAddressedEmail { get; set; } + public string CatchAllEmailDomain { get; set; } + public string FirefoxRelayApiAccessToken { get; set; } + public string SimpleLoginApiKey { get; set; } + public string DuckDuckGoApiKey { get; set; } + public string FastMailApiKey { get; set; } + public string AnonAddyApiAccessToken { get; set; } + public string AnonAddyDomainName { get; set; } + public string EmailWebsite { get; set; } + + public ForwarderOptions GetForwarderOptions() + { + if (Type != UsernameType.ForwardedEmailAlias) + { + return null; + } + + switch (ServiceType) + { + case ForwardedEmailServiceType.AnonAddy: + return new AnonAddyForwarderOptions + { + ApiKey = AnonAddyApiAccessToken, + DomainName = AnonAddyDomainName + }; + case ForwardedEmailServiceType.DuckDuckGo: + return new ForwarderOptions { ApiKey = DuckDuckGoApiKey }; + case ForwardedEmailServiceType.Fastmail: + return new ForwarderOptions { ApiKey = FastMailApiKey }; + case ForwardedEmailServiceType.FirefoxRelay: + return new ForwarderOptions { ApiKey = FirefoxRelayApiAccessToken }; + case ForwardedEmailServiceType.SimpleLogin: + return new ForwarderOptions { ApiKey = SimpleLoginApiKey }; + default: + return null; + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Export/Card.cs b/src/Maui/Bitwarden/Core/Models/Export/Card.cs new file mode 100644 index 000000000..ea5e41d74 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Export/Card.cs @@ -0,0 +1,52 @@ +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Export +{ + public class Card + { + public Card() { } + + public Card(CardView obj) + { + CardholderName = obj.CardholderName; + Brand = obj.Brand; + Number = obj.Number; + ExpMonth = obj.ExpMonth; + ExpYear = obj.ExpYear; + Code = obj.Code; + } + + public Card(Domain.Card obj) + { + CardholderName = obj.CardholderName?.EncryptedString; + Brand = obj.Brand?.EncryptedString; + Number = obj.Number?.EncryptedString; + ExpMonth = obj.ExpMonth?.EncryptedString; + ExpYear = obj.ExpYear?.EncryptedString; + Code = obj.Code?.EncryptedString; + } + + public string CardholderName { get; set; } + public string Brand { get; set; } + public string Number { get; set; } + public string ExpMonth { get; set; } + public string ExpYear { get; set; } + public string Code { get; set; } + + public static CardView ToView(Card req, CardView view = null) + { + if (view == null) + { + view = new CardView(); + } + + view.CardholderName = req.CardholderName; + view.Brand = req.Brand; + view.Number = req.Number; + view.ExpMonth = req.ExpMonth; + view.ExpYear = req.ExpYear; + view.Code = req.Code; + return view; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Export/Cipher.cs b/src/Maui/Bitwarden/Core/Models/Export/Cipher.cs new file mode 100644 index 000000000..70da4efb2 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Export/Cipher.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.Linq; +using Bit.Core.Enums; +using Bit.Core.Models.View; +using Newtonsoft.Json; + +namespace Bit.Core.Models.Export +{ + public class Cipher + { + public Cipher() { } + + public Cipher(CipherView obj) + { + OrganizationId = obj.OrganizationId; + FolderId = obj.FolderId; + Type = obj.Type; + Name = obj.Name; + Notes = obj.Notes; + Favorite = obj.Favorite; + + Fields = obj.Fields?.Select(f => new Field(f)).ToList(); + + switch (obj.Type) + { + case CipherType.Login: + Login = new Login(obj.Login); + break; + case CipherType.SecureNote: + SecureNote = new SecureNote(obj.SecureNote); + break; + case CipherType.Card: + Card = new Card(obj.Card); + break; + case CipherType.Identity: + Identity = new Identity(obj.Identity); + break; + } + } + + public Cipher(Domain.Cipher obj) + { + OrganizationId = obj.OrganizationId; + FolderId = obj.FolderId; + Type = obj.Type; + Name = obj.Name?.EncryptedString; + Notes = obj.Notes?.EncryptedString; + Favorite = obj.Favorite; + + Fields = obj.Fields?.Select(f => new Field(f)).ToList(); + + switch (obj.Type) + { + case CipherType.Login: + Login = new Login(obj.Login); + break; + case CipherType.SecureNote: + SecureNote = new SecureNote(obj.SecureNote); + break; + case CipherType.Card: + Card = new Card(obj.Card); + break; + case CipherType.Identity: + Identity = new Identity(obj.Identity); + break; + } + } + + public string OrganizationId { get; set; } + public string FolderId { get; set; } + public CipherType Type { get; set; } + public string Name { get; set; } + public string Notes { get; set; } + public bool Favorite { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public List Fields { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public Login Login { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public SecureNote SecureNote { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public Card Card { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public Identity Identity { get; set; } + + public CipherView ToView(Cipher req, CipherView view = null) + { + if (view == null) + { + view = new CipherView(); + } + + view.Type = req.Type; + view.FolderId = req.FolderId; + if (view.OrganizationId == null) + { + view.OrganizationId = req.OrganizationId; + } + + view.Name = req.Name; + view.Notes = req.Notes; + view.Favorite = req.Favorite; + + view.Fields = req.Fields?.Select(f => Field.ToView(f)).ToList(); + + switch (req.Type) + { + case CipherType.Login: + view.Login = Login.ToView(req.Login); + break; + case CipherType.SecureNote: + view.SecureNote = SecureNote.ToView(req.SecureNote); + break; + case CipherType.Card: + view.Card = Card.ToView(req.Card); + break; + case CipherType.Identity: + view.Identity = Identity.ToView(req.Identity); + break; + } + + return view; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Export/CipherWithId.cs b/src/Maui/Bitwarden/Core/Models/Export/CipherWithId.cs new file mode 100644 index 000000000..129267c05 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Export/CipherWithId.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Bit.Core.Models.View; +using Newtonsoft.Json; + +namespace Bit.Core.Models.Export +{ + public class CipherWithId : Cipher + { + public CipherWithId(CipherView obj) : base(obj) + { + Id = obj.Id; + CollectionIds = null; + } + + public CipherWithId(Domain.Cipher obj) : base(obj) + { + Id = obj.Id; + CollectionIds = null; + } + + [JsonProperty(Order = int.MinValue)] + public string Id { get; set; } + [JsonProperty(Order = int.MaxValue)] + public HashSet CollectionIds { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Export/Collection.cs b/src/Maui/Bitwarden/Core/Models/Export/Collection.cs new file mode 100644 index 000000000..7aa515322 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Export/Collection.cs @@ -0,0 +1,44 @@ +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Export +{ + public class Collection + { + public Collection() { } + + public Collection(CollectionView obj) + { + OrganizationId = obj.OrganizationId; + Name = obj.Name; + ExternalId = obj.ExternalId; + } + + public Collection(Domain.Collection obj) + { + OrganizationId = obj.OrganizationId; + Name = obj.Name?.EncryptedString; + ExternalId = obj.ExternalId; + } + + public string OrganizationId { get; set; } + public string Name { get; set; } + public string ExternalId { get; set; } + + public CollectionView ToView(Collection req, CollectionView view = null) + { + if (view == null) + { + view = new CollectionView(); + } + + view.Name = req.Name; + view.ExternalId = req.ExternalId; + if (view.OrganizationId == null) + { + view.OrganizationId = req.OrganizationId; + } + + return view; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Export/CollectionWithId.cs b/src/Maui/Bitwarden/Core/Models/Export/CollectionWithId.cs new file mode 100644 index 000000000..01e80f250 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Export/CollectionWithId.cs @@ -0,0 +1,21 @@ +using Bit.Core.Models.View; +using Newtonsoft.Json; + +namespace Bit.Core.Models.Export +{ + public class CollectionWithId : Collection + { + public CollectionWithId(CollectionView obj) : base(obj) + { + Id = obj.Id; + } + + public CollectionWithId(Domain.Collection obj) : base(obj) + { + Id = obj.Id; + } + + [JsonProperty(Order = int.MinValue)] + public string Id { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Export/Field.cs b/src/Maui/Bitwarden/Core/Models/Export/Field.cs new file mode 100644 index 000000000..373b8beb9 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Export/Field.cs @@ -0,0 +1,41 @@ +using Bit.Core.Enums; +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Export +{ + public class Field + { + public Field() { } + + public Field(FieldView obj) + { + Name = obj.Name; + Value = obj.Value; + Type = obj.Type; + } + + public Field(Domain.Field obj) + { + Name = obj.Name?.EncryptedString; + Value = obj.Value?.EncryptedString; + Type = obj.Type; + } + + public string Name { get; set; } + public string Value { get; set; } + public FieldType Type { get; set; } + + public static FieldView ToView(Field req, FieldView view = null) + { + if (view == null) + { + view = new FieldView(); + } + + view.Type = req.Type; + view.Value = req.Value; + view.Name = req.Name; + return view; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Export/Folder.cs b/src/Maui/Bitwarden/Core/Models/Export/Folder.cs new file mode 100644 index 000000000..8f9d71b98 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Export/Folder.cs @@ -0,0 +1,32 @@ +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Export +{ + public class Folder + { + public Folder() { } + + public Folder(FolderView obj) + { + Name = obj.Name; + } + + public Folder(Domain.Folder obj) + { + Name = obj.Name?.EncryptedString; + } + + public string Name { get; set; } + + public FolderView ToView(Folder req, FolderView view = null) + { + if (view == null) + { + view = new FolderView(); + } + + view.Name = req.Name; + return view; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Export/FolderWithId.cs b/src/Maui/Bitwarden/Core/Models/Export/FolderWithId.cs new file mode 100644 index 000000000..33fd180a7 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Export/FolderWithId.cs @@ -0,0 +1,21 @@ +using Bit.Core.Models.View; +using Newtonsoft.Json; + +namespace Bit.Core.Models.Export +{ + public class FolderWithId : Folder + { + public FolderWithId(FolderView obj) : base(obj) + { + Id = obj.Id; + } + + public FolderWithId(Domain.Folder obj) : base(obj) + { + Id = obj.Id; + } + + [JsonProperty(Order = int.MinValue)] + public string Id { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Export/Identity.cs b/src/Maui/Bitwarden/Core/Models/Export/Identity.cs new file mode 100644 index 000000000..d3d223901 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Export/Identity.cs @@ -0,0 +1,100 @@ +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Export +{ + public class Identity + { + public Identity() { } + + public Identity(IdentityView obj) + { + Title = obj.Title; + FirstName = obj.FirstName; + MiddleName = obj.MiddleName; + LastName = obj.LastName; + Address1 = obj.Address1; + Address2 = obj.Address2; + Address3 = obj.Address3; + City = obj.City; + State = obj.State; + PostalCode = obj.PostalCode; + Country = obj.Country; + Company = obj.Company; + Email = obj.Email; + Phone = obj.Phone; + SSN = obj.SSN; + Username = obj.Username; + PassportNumber = obj.PassportNumber; + LicenseNumber = obj.LicenseNumber; + } + + public Identity(Domain.Identity obj) + { + Title = obj.Title?.EncryptedString; + FirstName = obj.FirstName?.EncryptedString; + MiddleName = obj.FirstName?.EncryptedString; + LastName = obj.LastName?.EncryptedString; + Address1 = obj.Address1?.EncryptedString; + Address2 = obj.Address2?.EncryptedString; + Address3 = obj.Address3?.EncryptedString; + City = obj.City?.EncryptedString; + State = obj.State?.EncryptedString; + PostalCode = obj.PostalCode?.EncryptedString; + Country = obj.Country?.EncryptedString; + Company = obj.Company?.EncryptedString; + Email = obj.Email?.EncryptedString; + Phone = obj.Phone?.EncryptedString; + SSN = obj.SSN?.EncryptedString; + Username = obj.Username?.EncryptedString; + PassportNumber = obj.PassportNumber?.EncryptedString; + LicenseNumber = obj.LicenseNumber?.EncryptedString; + } + + public string Title { get; set; } + public string FirstName { get; set; } + public string MiddleName { get; set; } + public string LastName { get; set; } + public string Address1 { get; set; } + public string Address2 { get; set; } + public string Address3 { get; set; } + public string City { get; set; } + public string State { get; set; } + public string PostalCode { get; set; } + public string Country { get; set; } + public string Company { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public string SSN { get; set; } + public string Username { get; set; } + public string PassportNumber { get; set; } + public string LicenseNumber { get; set; } + + public static IdentityView ToView(Identity req, IdentityView view = null) + { + if (view == null) + { + view = new IdentityView(); + } + + view.Title = req.Title; + view.FirstName = req.FirstName; + view.MiddleName = req.MiddleName; + view.LastName = req.LastName; + view.Address1 = req.Address1; + view.Address2 = req.Address2; + view.Address3 = req.Address3; + view.City = req.City; + view.State = req.State; + view.PostalCode = req.PostalCode; + view.Country = req.Country; + view.Company = req.Company; + view.Email = req.Email; + view.Phone = req.Phone; + view.SSN = req.SSN; + view.Username = req.Username; + view.PassportNumber = req.PassportNumber; + view.LicenseNumber = req.LicenseNumber; + return view; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Export/Login.cs b/src/Maui/Bitwarden/Core/Models/Export/Login.cs new file mode 100644 index 000000000..883ec18be --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Export/Login.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Export +{ + public class Login + { + public Login() { } + + public Login(LoginView obj) + { + Uris = obj.Uris?.Select(u => new LoginUri(u)).ToList(); + + Username = obj.Username; + Password = obj.Password; + Totp = obj.Totp; + } + + public Login(Domain.Login obj) + { + Uris = obj.Uris?.Select(u => new LoginUri(u)).ToList(); + + Username = obj.Username?.EncryptedString; + Password = obj.Password?.EncryptedString; + Totp = obj.Totp?.EncryptedString; + } + + public List Uris { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string Totp { get; set; } + + public static LoginView ToView(Login req, LoginView view = null) + { + if (view == null) + { + view = new LoginView(); + } + + view.Uris = req.Uris?.Select(u => LoginUri.ToView(u)).ToList(); + + view.Username = req.Username; + view.Password = req.Password; + view.Totp = req.Totp; + return view; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Export/LoginUri.cs b/src/Maui/Bitwarden/Core/Models/Export/LoginUri.cs new file mode 100644 index 000000000..e3f215ff7 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Export/LoginUri.cs @@ -0,0 +1,37 @@ +using Bit.Core.Enums; +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Export +{ + public class LoginUri + { + public LoginUri() { } + + public LoginUri(LoginUriView obj) + { + Match = obj.Match; + Uri = obj.Uri; + } + + public LoginUri(Domain.LoginUri obj) + { + Match = obj.Match; + Uri = obj.Uri?.EncryptedString; + } + + public UriMatchType? Match { get; set; } + public string Uri { get; set; } + + public static LoginUriView ToView(LoginUri req, LoginUriView view = null) + { + if (view == null) + { + view = new LoginUriView(); + } + + view.Match = req.Match; + view.Uri = req.Uri; + return view; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Export/SecureNote.cs b/src/Maui/Bitwarden/Core/Models/Export/SecureNote.cs new file mode 100644 index 000000000..71270091c --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Export/SecureNote.cs @@ -0,0 +1,33 @@ +using Bit.Core.Enums; +using Bit.Core.Models.View; + +namespace Bit.Core.Models.Export +{ + public class SecureNote + { + public SecureNote() { } + + public SecureNote(SecureNoteView obj) + { + Type = obj.Type; + } + + public SecureNote(Domain.SecureNote obj) + { + Type = obj.Type; + } + + public SecureNoteType Type { get; set; } + + public SecureNoteView ToView(SecureNote req, SecureNoteView view = null) + { + if (view == null) + { + view = new SecureNoteView(); + } + + view.Type = req.Type; + return view; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/AttachmentRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/AttachmentRequest.cs new file mode 100644 index 000000000..fd11fb541 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/AttachmentRequest.cs @@ -0,0 +1,11 @@ +using System; + +namespace Bit.Core.Models.Request +{ + public class AttachmentRequest + { + public string FileName { get; set; } + public string Key { get; set; } + public long FileSize { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/CipherCollectionsRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/CipherCollectionsRequest.cs new file mode 100644 index 000000000..f5659ef73 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/CipherCollectionsRequest.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Request +{ + public class CipherCollectionsRequest + { + public CipherCollectionsRequest(List collectionIds) + { + CollectionIds = collectionIds ?? new List(); + } + + public List CollectionIds { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/CipherCreateRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/CipherCreateRequest.cs new file mode 100644 index 000000000..bee9b3c64 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/CipherCreateRequest.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.Request +{ + public class CipherCreateRequest + { + public CipherCreateRequest(Cipher cipher) + { + Cipher = new CipherRequest(cipher); + CollectionIds = cipher.CollectionIds?.ToList(); + } + + public CipherRequest Cipher { get; set; } + public List CollectionIds { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/CipherRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/CipherRequest.cs new file mode 100644 index 000000000..121abc2f8 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/CipherRequest.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Bit.Core.Enums; +using Bit.Core.Models.Api; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.Request +{ + public class CipherRequest + { + public CipherRequest(Cipher cipher) + { + Type = cipher.Type; + OrganizationId = cipher.OrganizationId; + FolderId = cipher.FolderId; + Name = cipher.Name?.EncryptedString; + Notes = cipher.Notes?.EncryptedString; + Favorite = cipher.Favorite; + LastKnownRevisionDate = cipher.RevisionDate; + Reprompt = cipher.Reprompt; + + switch (Type) + { + case CipherType.Login: + Login = new LoginApi + { + Uris = cipher.Login.Uris?.Select( + u => new LoginUriApi { Match = u.Match, Uri = u.Uri?.EncryptedString }).ToList(), + Username = cipher.Login.Username?.EncryptedString, + Password = cipher.Login.Password?.EncryptedString, + PasswordRevisionDate = cipher.Login.PasswordRevisionDate, + Totp = cipher.Login.Totp?.EncryptedString, + Fido2Key = cipher.Login.Fido2Key != null ? new Fido2KeyApi(cipher.Login.Fido2Key) : null + }; + break; + case CipherType.Card: + Card = new CardApi + { + CardholderName = cipher.Card.CardholderName?.EncryptedString, + Brand = cipher.Card.Brand?.EncryptedString, + Number = cipher.Card.Number?.EncryptedString, + ExpMonth = cipher.Card.ExpMonth?.EncryptedString, + ExpYear = cipher.Card.ExpYear?.EncryptedString, + Code = cipher.Card.Code?.EncryptedString + }; + break; + case CipherType.Identity: + Identity = new IdentityApi + { + Title = cipher.Identity.Title?.EncryptedString, + FirstName = cipher.Identity.FirstName?.EncryptedString, + MiddleName = cipher.Identity.MiddleName?.EncryptedString, + LastName = cipher.Identity.LastName?.EncryptedString, + Address1 = cipher.Identity.Address1?.EncryptedString, + Address2 = cipher.Identity.Address2?.EncryptedString, + Address3 = cipher.Identity.Address3?.EncryptedString, + City = cipher.Identity.City?.EncryptedString, + State = cipher.Identity.State?.EncryptedString, + PostalCode = cipher.Identity.PostalCode?.EncryptedString, + Country = cipher.Identity.Country?.EncryptedString, + Company = cipher.Identity.Company?.EncryptedString, + Email = cipher.Identity.Email?.EncryptedString, + Phone = cipher.Identity.Phone?.EncryptedString, + SSN = cipher.Identity.SSN?.EncryptedString, + Username = cipher.Identity.Username?.EncryptedString, + PassportNumber = cipher.Identity.PassportNumber?.EncryptedString, + LicenseNumber = cipher.Identity.LicenseNumber?.EncryptedString + }; + break; + case CipherType.SecureNote: + SecureNote = new SecureNoteApi + { + Type = cipher.SecureNote.Type + }; + break; + case CipherType.Fido2Key: + Fido2Key = new Fido2KeyApi(cipher.Fido2Key); + break; + default: + break; + } + + Fields = cipher.Fields?.Select(f => new FieldApi + { + Type = f.Type, + Name = f.Name?.EncryptedString, + Value = f.Value?.EncryptedString, + LinkedId = f.LinkedId, + }).ToList(); + + PasswordHistory = cipher.PasswordHistory?.Select(ph => new PasswordHistoryRequest + { + Password = ph.Password?.EncryptedString, + LastUsedDate = ph.LastUsedDate + }).ToList(); + + if (cipher.Attachments != null) + { + Attachments = new Dictionary(); + Attachments2 = new Dictionary(); + foreach (var attachment in cipher.Attachments) + { + var fileName = attachment.FileName?.EncryptedString; + Attachments.Add(attachment.Id, fileName); + Attachments2.Add(attachment.Id, new AttachmentRequest + { + FileName = fileName, + Key = attachment.Key?.EncryptedString + }); + } + } + } + + public CipherType Type { get; set; } + public string OrganizationId { get; set; } + public string FolderId { get; set; } + public string Name { get; set; } + public string Notes { get; set; } + public bool Favorite { get; set; } + public LoginApi Login { get; set; } + public SecureNoteApi SecureNote { get; set; } + public CardApi Card { get; set; } + public IdentityApi Identity { get; set; } + public Fido2KeyApi Fido2Key { get; set; } + public List Fields { get; set; } + public List PasswordHistory { get; set; } + public Dictionary Attachments { get; set; } + public Dictionary Attachments2 { get; set; } + public DateTime LastKnownRevisionDate { get; set; } + public CipherRepromptType Reprompt { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/CipherShareRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/CipherShareRequest.cs new file mode 100644 index 000000000..89bfb6dc6 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/CipherShareRequest.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.Request +{ + public class CipherShareRequest + { + public CipherShareRequest(Cipher cipher) + { + Cipher = new CipherRequest(cipher); + CollectionIds = cipher.CollectionIds?.ToList(); + } + + public CipherRequest Cipher { get; set; } + public List CollectionIds { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/DeleteAccountRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/DeleteAccountRequest.cs new file mode 100644 index 000000000..8eb38d347 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/DeleteAccountRequest.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Models.Request +{ + public class DeleteAccountRequest + { + public string MasterPasswordHash { get; set; } + + public string OTP { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/DeviceRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/DeviceRequest.cs new file mode 100644 index 000000000..a4cd687bb --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/DeviceRequest.cs @@ -0,0 +1,20 @@ +using Bit.Core.Abstractions; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Request +{ + public class DeviceRequest + { + public DeviceRequest(string appId, IPlatformUtilsService platformUtilsService) + { + Type = platformUtilsService.GetDevice(); + Name = platformUtilsService.GetDeviceString(); + Identifier = appId; + } + + public DeviceType? Type { get; set; } + public string Name { get; set; } + public string Identifier { get; set; } + public string PushToken { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/DeviceTokenRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/DeviceTokenRequest.cs new file mode 100644 index 000000000..8806f3b0a --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/DeviceTokenRequest.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Request +{ + public class DeviceTokenRequest + { + public string PushToken { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/EventRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/EventRequest.cs new file mode 100644 index 000000000..da4124189 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/EventRequest.cs @@ -0,0 +1,12 @@ +using System; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Request +{ + public class EventRequest + { + public EventType Type { get; set; } + public string CipherId { get; set; } + public DateTime Date { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/FolderRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/FolderRequest.cs new file mode 100644 index 000000000..a09b5134e --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/FolderRequest.cs @@ -0,0 +1,14 @@ +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.Request +{ + public class FolderRequest + { + public FolderRequest(Folder folder) + { + Name = folder.Name?.EncryptedString; + } + + public string Name { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/KeyConnectorUserKeyRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/KeyConnectorUserKeyRequest.cs new file mode 100644 index 000000000..13870979e --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/KeyConnectorUserKeyRequest.cs @@ -0,0 +1,13 @@ +using System; +namespace Bit.Core.Models.Request +{ + public class KeyConnectorUserKeyRequest + { + public string Key { get; set; } + + public KeyConnectorUserKeyRequest(string key) + { + Key = key; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/KeysRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/KeysRequest.cs new file mode 100644 index 000000000..9cae0a89d --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/KeysRequest.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Models.Request +{ + public class KeysRequest + { + public string PublicKey { get; set; } + public string EncryptedPrivateKey { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/OrganizationSsoDomainDetailsRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/OrganizationSsoDomainDetailsRequest.cs new file mode 100644 index 000000000..c241d21a1 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/OrganizationSsoDomainDetailsRequest.cs @@ -0,0 +1,9 @@ +using System; +namespace Bit.Core.Models.Request +{ + public class OrganizationDomainSsoDetailsRequest + { + public string Email { get; set; } + } +} + diff --git a/src/Maui/Bitwarden/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs new file mode 100644 index 000000000..663e0fc61 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Models.Request +{ + public class OrganizationUserResetPasswordEnrollmentRequest + { + public string MasterPasswordHash { get; set; } + public string ResetPasswordKey { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/PasswordHintRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/PasswordHintRequest.cs new file mode 100644 index 000000000..bd3389abb --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/PasswordHintRequest.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Request +{ + public class PasswordHintRequest + { + public string Email { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/PasswordHistoryRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/PasswordHistoryRequest.cs new file mode 100644 index 000000000..33e323890 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/PasswordHistoryRequest.cs @@ -0,0 +1,10 @@ +using System; + +namespace Bit.Core.Models.Request +{ + public class PasswordHistoryRequest + { + public string Password { get; set; } + public DateTime? LastUsedDate { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/PasswordRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/PasswordRequest.cs new file mode 100644 index 000000000..2a65bc197 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/PasswordRequest.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Models.Request +{ + public class PasswordRequest + { + public string MasterPasswordHash { get; set; } + public string NewMasterPasswordHash { get; set; } + public string MasterPasswordHint { get; set; } + public string Key { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/PasswordVerificationRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/PasswordVerificationRequest.cs new file mode 100644 index 000000000..bb37b043c --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/PasswordVerificationRequest.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Request +{ + public class PasswordVerificationRequest + { + public string MasterPasswordHash { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/PasswordlessCreateLoginRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/PasswordlessCreateLoginRequest.cs new file mode 100644 index 000000000..aeaff5f1f --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/PasswordlessCreateLoginRequest.cs @@ -0,0 +1,34 @@ +using System; +namespace Bit.Core.Models.Request +{ + public class PasswordlessCreateLoginRequest + { + public PasswordlessCreateLoginRequest(string email, string publicKey, string deviceIdentifier, string accessCode, AuthRequestType? type, string fingerprintPhrase) + { + Email = email ?? throw new ArgumentNullException(nameof(email)); + PublicKey = publicKey ?? throw new ArgumentNullException(nameof(publicKey)); + DeviceIdentifier = deviceIdentifier ?? throw new ArgumentNullException(nameof(deviceIdentifier)); + AccessCode = accessCode ?? throw new ArgumentNullException(nameof(accessCode)); + Type = type; + FingerprintPhrase = fingerprintPhrase ?? throw new ArgumentNullException(nameof(fingerprintPhrase)); + } + + public string Email { get; set; } + + public string PublicKey { get; set; } + + public string DeviceIdentifier { get; set; } + + public string AccessCode { get; set; } + + public AuthRequestType? Type { get; set; } + + public string FingerprintPhrase { get; set; } + } + + public enum AuthRequestType : byte + { + AuthenticateAndUnlock = 0, + Unlock = 1 + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/PasswordlessLoginRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/PasswordlessLoginRequest.cs new file mode 100644 index 000000000..1ed5a380c --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/PasswordlessLoginRequest.cs @@ -0,0 +1,21 @@ +using System; + +namespace Bit.Core.Models.Request +{ + public class PasswordlessLoginRequest + { + public PasswordlessLoginRequest(string key, string masterPasswordHash, string deviceIdentifier, + bool requestApproved) + { + Key = key ?? throw new ArgumentNullException(nameof(key)); + MasterPasswordHash = masterPasswordHash ?? throw new ArgumentNullException(nameof(masterPasswordHash)); + DeviceIdentifier = deviceIdentifier ?? throw new ArgumentNullException(nameof(deviceIdentifier)); + RequestApproved = requestApproved; + } + + public string Key { get; set; } + public string MasterPasswordHash { get; set; } + public string DeviceIdentifier { get; set; } + public bool RequestApproved { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/PreloginRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/PreloginRequest.cs new file mode 100644 index 000000000..13097a5b9 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/PreloginRequest.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Request +{ + public class PreloginRequest + { + public string Email { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/RegisterRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/RegisterRequest.cs new file mode 100644 index 000000000..92b5d3c76 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/RegisterRequest.cs @@ -0,0 +1,22 @@ +using System; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Request +{ + public class RegisterRequest + { + public string Name { get; set; } + public string Email { get; set; } + public string MasterPasswordHash { get; set; } + public string MasterPasswordHint { get; set; } + public string Key { get; set; } + public KeysRequest Keys { get; set; } + public string Token { get; set; } + public Guid? OrganizationUserId { get; set; } + public KdfType? Kdf { get; set; } + public int? KdfIterations { get; set; } + public int? KdfMemory { get; set; } + public int? KdfParallelism { get; set; } + public string CaptchaResponse { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/SendRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/SendRequest.cs new file mode 100644 index 000000000..abd7323ec --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/SendRequest.cs @@ -0,0 +1,58 @@ +using System; +using Bit.Core.Enums; +using Bit.Core.Models.Api; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.Request +{ + public class SendRequest + { + public SendType Type { get; set; } + public long? FileLength { get; set; } + public string Name { get; set; } + public string Notes { get; set; } + public string Key { get; set; } + public int? MaxAccessCount { get; set; } + public DateTime? ExpirationDate { get; set; } + public DateTime DeletionDate { get; set; } + public SendTextApi Text { get; set; } + public SendFileApi File { get; set; } + public string Password { get; set; } + public bool Disabled { get; set; } + public bool HideEmail { get; set; } + + public SendRequest(Send send, long? fileLength) + { + Type = send.Type; + FileLength = fileLength; + Name = send.Name?.EncryptedString; + Notes = send.Notes?.EncryptedString; + MaxAccessCount = send.MaxAccessCount; + ExpirationDate = send.ExpirationDate; + DeletionDate = send.DeletionDate; + Key = send.Key?.EncryptedString; + Password = send.Password; + Disabled = send.Disabled; + HideEmail = send.HideEmail; + + switch (Type) + { + case SendType.Text: + Text = new SendTextApi + { + Text = send.Text?.Text?.EncryptedString, + Hidden = send.Text.Hidden + }; + break; + case SendType.File: + File = new SendFileApi + { + FileName = send.File?.FileName?.EncryptedString + }; + break; + default: + break; + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/SetKeyConnectorKeyRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/SetKeyConnectorKeyRequest.cs new file mode 100644 index 000000000..3790a8b2c --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/SetKeyConnectorKeyRequest.cs @@ -0,0 +1,27 @@ +using System; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Request +{ + public class SetKeyConnectorKeyRequest + { + public string Key { get; set; } + public KeysRequest Keys { get; set; } + public KdfType Kdf { get; set; } + public int? KdfIterations { get; set; } + public int? KdfMemory { get; set; } + public int? KdfParallelism { get; set; } + public string OrgIdentifier { get; set; } + + public SetKeyConnectorKeyRequest(string key, KeysRequest keys, KdfConfig kdfConfig, string orgIdentifier) + { + this.Key = key; + this.Keys = keys; + this.Kdf = kdfConfig.Type.GetValueOrDefault(KdfType.PBKDF2_SHA256); + this.KdfIterations = kdfConfig.Iterations; + this.KdfMemory = kdfConfig.Memory; + this.KdfParallelism = kdfConfig.Parallelism; + this.OrgIdentifier = orgIdentifier; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/SetPasswordRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/SetPasswordRequest.cs new file mode 100644 index 000000000..d41eb2249 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/SetPasswordRequest.cs @@ -0,0 +1,17 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Models.Request +{ + public class SetPasswordRequest + { + public string MasterPasswordHash { get; set; } + public string Key { get; set; } + public string MasterPasswordHint { get; set; } + public KeysRequest Keys { get; set; } + public KdfType Kdf { get; set; } + public int KdfIterations { get; set; } + public int? KdfMemory { get; set; } + public int? KdfParallelism { get; set; } + public string OrgIdentifier { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/TokenRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/TokenRequest.cs new file mode 100644 index 000000000..75d14fdb9 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/TokenRequest.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Net.Http.Headers; +using System.Text; +using Bit.Core.Enums; +using Bit.Core.Utilities; + +namespace Bit.Core.Models.Request +{ + public class TokenRequest + { + public string Email { get; set; } + public string MasterPasswordHash { get; set; } + public string Code { get; set; } + public string CodeVerifier { get; set; } + public string RedirectUri { get; set; } + public string Token { get; set; } + public string AuthRequestId { get; set; } + public TwoFactorProviderType? Provider { get; set; } + public bool? Remember { get; set; } + public string CaptchaToken { get; set; } + public DeviceRequest Device { get; set; } + + public TokenRequest(string[] credentials, string[] codes, TwoFactorProviderType? provider, string token, + bool? remember, string captchaToken, DeviceRequest device = null, string authRequestId = null) + { + if (credentials != null && credentials.Length > 1) + { + Email = credentials[0]; + MasterPasswordHash = credentials[1]; + } + else if (codes != null && codes.Length > 2) + { + Code = codes[0]; + CodeVerifier = codes[1]; + RedirectUri = codes[2]; + } + Token = token; + Provider = provider; + Remember = remember; + Device = device; + CaptchaToken = captchaToken; + AuthRequestId = authRequestId; + } + + public Dictionary ToIdentityToken(string clientId) + { + var obj = new Dictionary + { + ["scope"] = "api offline_access", + ["client_id"] = clientId + }; + + if (MasterPasswordHash != null && Email != null) + { + obj.Add("grant_type", "password"); + obj.Add("username", Email); + obj.Add("password", MasterPasswordHash); + } + else if (Code != null && CodeVerifier != null && RedirectUri != null) + { + obj.Add("grant_type", "authorization_code"); + obj.Add("code", Code); + obj.Add("code_verifier", CodeVerifier); + obj.Add("redirect_uri", RedirectUri); + } + else + { + throw new Exception("must provide credentials or codes"); + } + + if (AuthRequestId != null) + { + obj.Add("authRequest", AuthRequestId); + } + + if (Device != null) + { + obj.Add("deviceType", ((int)Device.Type).ToString()); + obj.Add("deviceIdentifier", Device.Identifier); + obj.Add("deviceName", Device.Name); + obj.Add("devicePushToken", Device.PushToken); + } + if (!string.IsNullOrWhiteSpace(Token) && Provider != null && Remember.HasValue) + { + obj.Add("twoFactorToken", Token); + obj.Add("twoFactorProvider", ((int)Provider.Value).ToString()); + obj.Add("twoFactorRemember", Remember.GetValueOrDefault() ? "1" : "0"); + } + if (CaptchaToken != null) + { + obj.Add("captchaResponse", CaptchaToken); + } + + return obj; + } + + public void AlterIdentityTokenHeaders(HttpRequestHeaders headers) + { + if (MasterPasswordHash != null && Email != null) + { + headers.Add("Auth-Email", CoreHelpers.Base64UrlEncode(Encoding.UTF8.GetBytes(Email))); + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/TwoFactorEmailRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/TwoFactorEmailRequest.cs new file mode 100644 index 000000000..2b5784099 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/TwoFactorEmailRequest.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Models.Request +{ + public class TwoFactorEmailRequest + { + public string Email { get; set; } + public string MasterPasswordHash { get; set; } + public string DeviceIdentifier { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/UpdateTempPasswordRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/UpdateTempPasswordRequest.cs new file mode 100644 index 000000000..1c3ddf009 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/UpdateTempPasswordRequest.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Models.Request +{ + public class UpdateTempPasswordRequest + { + public string NewMasterPasswordHash { get; set; } + public string MasterPasswordHint { get; set; } + public string Key { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Request/VerifyOTPRequest.cs b/src/Maui/Bitwarden/Core/Models/Request/VerifyOTPRequest.cs new file mode 100644 index 000000000..11dc1b989 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Request/VerifyOTPRequest.cs @@ -0,0 +1,13 @@ +using System; +namespace Bit.Core.Models.Request +{ + public class VerifyOTPRequest + { + public string OTP; + + public VerifyOTPRequest(string otp) + { + OTP = otp; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/AttachmentResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/AttachmentResponse.cs new file mode 100644 index 000000000..1dea2a877 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/AttachmentResponse.cs @@ -0,0 +1,12 @@ +namespace Bit.Core.Models.Response +{ + public class AttachmentResponse + { + public string Id { get; set; } + public string Url { get; set; } + public string FileName { get; set; } + public string Key { get; set; } + public string Size { get; set; } + public string SizeName { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/AttachmentUploadDataReponse.cs b/src/Maui/Bitwarden/Core/Models/Response/AttachmentUploadDataReponse.cs new file mode 100644 index 000000000..e26059d9f --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/AttachmentUploadDataReponse.cs @@ -0,0 +1,12 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Models.Response +{ + public class AttachmentUploadDataResponse + { + public string AttachmentId { get; set; } + public FileUploadType FileUploadType { get; set; } + public CipherResponse CipherResponse { get; set; } + public string Url { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/BreachAccountResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/BreachAccountResponse.cs new file mode 100644 index 000000000..f32ba923a --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/BreachAccountResponse.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Response +{ + public class BreachAccountResponse + { + public string AddedDate { get; set; } + public string BreachDate { get; set; } + public List DataClasses { get; set; } + public string Description { get; set; } + public string Domain { get; set; } + public bool IsActive { get; set; } + public bool IsVerified { get; set; } + public string LogoPath { get; set; } + public string ModifiedDate { get; set; } + public string Name { get; set; } + public int PwnCount { get; set; } + public string Title { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/CipherResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/CipherResponse.cs new file mode 100644 index 000000000..5247c4522 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/CipherResponse.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using Bit.Core.Enums; +using Bit.Core.Models.Api; + +namespace Bit.Core.Models.Response +{ + public class CipherResponse + { + public string Id { get; set; } + public string OrganizationId { get; set; } + public string FolderId { get; set; } + public Enums.CipherType Type { get; set; } + public string Name { get; set; } + public string Notes { get; set; } + public List Fields { get; set; } + public LoginApi Login { get; set; } + public CardApi Card { get; set; } + public IdentityApi Identity { get; set; } + public SecureNoteApi SecureNote { get; set; } + public Fido2KeyApi Fido2Key { get; set; } + public bool Favorite { get; set; } + public bool Edit { get; set; } + public bool ViewPassword { get; set; } = true; // Fallback for old server versions + public bool OrganizationUseTotp { get; set; } + public DateTime RevisionDate { get; set; } + public List Attachments { get; set; } + public List PasswordHistory { get; set; } + public List CollectionIds { get; set; } + public DateTime? DeletedDate { get; set; } + public CipherRepromptType Reprompt { get; set; } + public DateTime CreationDate { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/CollectionResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/CollectionResponse.cs new file mode 100644 index 000000000..7895ecb7f --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/CollectionResponse.cs @@ -0,0 +1,15 @@ +namespace Bit.Core.Models.Response +{ + public class CollectionResponse + { + public string Id { get; set; } + public string OrganizationId { get; set; } + public string Name { get; set; } + public string ExternalId { get; set; } + } + + public class CollectionDetailsResponse : CollectionResponse + { + public bool ReadOnly { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/ConfigResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/ConfigResponse.cs new file mode 100644 index 000000000..d171a4bad --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/ConfigResponse.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; + +namespace Bit.Core.Models.Response +{ + public class ConfigResponse + { + public string Version { get; set; } + public string GitHash { get; set; } + public ServerConfigResponse Server { get; set; } + public EnvironmentConfigResponse Environment { get; set; } + public IDictionary FeatureStates { get; set; } + public DateTime ExpiresOn { get; set; } + } + + public class ServerConfigResponse + { + public string Name { get; set; } + public string Url { get; set; } + } + + public class EnvironmentConfigResponse + { + public string Vault { get; set; } + public string Api { get; set; } + public string Identity { get; set; } + public string Notifications { get; set; } + public string Sso { get; set; } + } +} + diff --git a/src/Maui/Bitwarden/Core/Models/Response/DomainsResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/DomainsResponse.cs new file mode 100644 index 000000000..ecdd0909c --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/DomainsResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Response +{ + public class DomainsResponse + { + public List> EquivalentDomains { get; set; } + public List GlobalEquivalentDomains { get; set; } = new List(); + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/ErrorResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/ErrorResponse.cs new file mode 100644 index 000000000..7d43ed1cc --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/ErrorResponse.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using Newtonsoft.Json.Linq; + +namespace Bit.Core.Models.Response +{ + public class ErrorResponse + { + public ErrorResponse() { } + + public ErrorResponse(JObject response, HttpStatusCode status, bool identityResponse = false) + { + JObject errorModel = null; + if (response != null) + { + var responseErrorModel = response.GetValue("ErrorModel", StringComparison.OrdinalIgnoreCase); + if (responseErrorModel != null && identityResponse) + { + errorModel = responseErrorModel.Value(); ; + } + else + { + errorModel = response; + } + } + if (errorModel != null) + { + var model = errorModel.ToObject(); + Message = model.Message; + ValidationErrors = model.ValidationErrors ?? new Dictionary>(); + CaptchaSiteKey = ValidationErrors.ContainsKey("HCaptcha_SiteKey") ? + ValidationErrors["HCaptcha_SiteKey"]?.FirstOrDefault() : + null; + CaptchaRequired = !string.IsNullOrWhiteSpace(CaptchaSiteKey); + } + else + { + if ((int)status == 429) + { + Message = "Rate limit exceeded. Try again later."; + } + } + StatusCode = status; + } + + public string Message { get; set; } + public Dictionary> ValidationErrors { get; set; } + public HttpStatusCode StatusCode { get; set; } + public string CaptchaSiteKey { get; set; } + public bool CaptchaRequired { get; set; } = false; + + public string GetSingleMessage() + { + if (ValidationErrors == null) + { + return Message; + } + foreach (var error in ValidationErrors) + { + if (error.Value?.Any() ?? false) + { + return error.Value[0]; + } + } + return Message; + } + + public string GetFullMessage() + { + string GetDefaultMessage() => $"{(int)StatusCode} {StatusCode}. {Message}"; + + if (ValidationErrors is null) + { + return GetDefaultMessage(); + } + + var valErrors = ValidationErrors.Where(e => e.Value != null && e.Value.Any()).ToList(); + if (!valErrors.Any()) + { + return GetDefaultMessage(); + } + + string GetFullError(string key, List errors) + { + return new StringBuilder() + .Append($"[{key}]: ") + .Append(string.Join("; ", errors)) + .ToString(); + }; + + return $"{(int)StatusCode} {StatusCode}. {string.Join(Environment.NewLine, valErrors.Select(ve => GetFullError(ve.Key, ve.Value)))}"; + } + + private class ErrorModel + { + public string Message { get; set; } + public Dictionary> ValidationErrors { get; set; } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/FolderResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/FolderResponse.cs new file mode 100644 index 000000000..e62fda4fb --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/FolderResponse.cs @@ -0,0 +1,11 @@ +using System; + +namespace Bit.Core.Models.Response +{ + public class FolderResponse + { + public string Id { get; set; } + public string Name { get; set; } + public DateTime RevisionDate { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/GlobalDomainResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/GlobalDomainResponse.cs new file mode 100644 index 000000000..354a9cf4b --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/GlobalDomainResponse.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Response +{ + public class GlobalDomainResponse + { + public int Type { get; set; } + public List Domains { get; set; } + public bool Excluded { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/IdentityCaptchaResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/IdentityCaptchaResponse.cs new file mode 100644 index 000000000..a5350e8da --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/IdentityCaptchaResponse.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Bit.Core.Enums; +using Newtonsoft.Json; + +namespace Bit.Core.Models.Response +{ + public class IdentityCaptchaResponse + { + [JsonProperty("HCaptcha_SiteKey")] + public string SiteKey { get; set; } + } + +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/IdentityResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/IdentityResponse.cs new file mode 100644 index 000000000..cb843fd8d --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/IdentityResponse.cs @@ -0,0 +1,50 @@ +using System.Net; +using Newtonsoft.Json.Linq; + +namespace Bit.Core.Models.Response +{ + public class IdentityResponse + { + public IdentityTokenResponse TokenResponse { get; } + public IdentityTwoFactorResponse TwoFactorResponse { get; } + public IdentityCaptchaResponse CaptchaResponse { get; } + + public bool TwoFactorNeeded => TwoFactorResponse != null; + public bool FailedToParse { get; } + + public IdentityResponse(HttpStatusCode httpStatusCode, JObject responseJObject) + { + var parsed = false; + if (responseJObject != null) + { + if (IsSuccessStatusCode(httpStatusCode)) + { + TokenResponse = responseJObject.ToObject(); + parsed = true; + } + else if (httpStatusCode == HttpStatusCode.BadRequest) + { + if (JObjectHasProperty(responseJObject, "TwoFactorProviders2")) + { + TwoFactorResponse = responseJObject.ToObject(); + parsed = true; + } + else if (JObjectHasProperty(responseJObject, "HCaptcha_SiteKey")) + { + CaptchaResponse = responseJObject.ToObject(); + parsed = true; + } + } + } + FailedToParse = !parsed; + } + + private bool IsSuccessStatusCode(HttpStatusCode httpStatusCode) => + (int)httpStatusCode >= 200 && (int)httpStatusCode < 300; + + private bool JObjectHasProperty(JObject jObject, string propertyName) => + jObject.ContainsKey(propertyName) && + jObject[propertyName] != null && + (jObject[propertyName].HasValues || jObject[propertyName].Value() != null); + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/IdentityTokenResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/IdentityTokenResponse.cs new file mode 100644 index 000000000..55d49bd24 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/IdentityTokenResponse.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Newtonsoft.Json; + +namespace Bit.Core.Models.Response +{ + public class IdentityTokenResponse + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + [JsonProperty("expires_in")] + public string ExpiresIn { get; set; } + [JsonProperty("refresh_token")] + public string RefreshToken { get; set; } + [JsonProperty("token_type")] + public string TokenType { get; set; } + + public bool ResetMasterPassword { get; set; } + public string PrivateKey { get; set; } + public string Key { get; set; } + public string TwoFactorToken { get; set; } + public KdfType Kdf { get; set; } + public int? KdfIterations { get; set; } + public int? KdfMemory { get; set; } + public int? KdfParallelism { get; set; } + public bool ForcePasswordReset { get; set; } + public string KeyConnectorUrl { get; set; } + public MasterPasswordPolicyOptions MasterPasswordPolicy { get; set; } + [JsonIgnore] + public KdfConfig KdfConfig => new KdfConfig(Kdf, KdfIterations, KdfMemory, KdfParallelism); + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/IdentityTwoFactorResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/IdentityTwoFactorResponse.cs new file mode 100644 index 000000000..3607a3230 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/IdentityTwoFactorResponse.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Newtonsoft.Json; + +namespace Bit.Core.Models.Response +{ + public class IdentityTwoFactorResponse + { + public List TwoFactorProviders { get; set; } + public Dictionary> TwoFactorProviders2 { get; set; } + public MasterPasswordPolicyOptions MasterPasswordPolicy { get; set; } + [JsonProperty("CaptchaBypassToken")] + public string CaptchaToken { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/KeyConnectorUserKeyResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/KeyConnectorUserKeyResponse.cs new file mode 100644 index 000000000..210cd349e --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/KeyConnectorUserKeyResponse.cs @@ -0,0 +1,9 @@ +using System; + +namespace Bit.Core.Models.Response +{ + public class KeyConnectorUserKeyResponse + { + public string Key { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/NotificationResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/NotificationResponse.cs new file mode 100644 index 000000000..ae5405d9b --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/NotificationResponse.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Response +{ + public class NotificationResponse + { + public string ContextId { get; set; } + public NotificationType Type { get; set; } + public string Payload { get; set; } + public object PayloadObject { get; set; } + } + + public class SyncCipherNotification + { + public string Id { get; set; } + public string UserId { get; set; } + public string OrganizationId { get; set; } + public HashSet CollectionIds { get; set; } + public DateTime RevisionDate { get; set; } + } + + public class SyncFolderNotification + { + public string Id { get; set; } + public string UserId { get; set; } + public DateTime RevisionDate { get; set; } + } + + public class UserNotification + { + public string UserId { get; set; } + public DateTime Date { get; set; } + } + + public class PasswordlessRequestNotification + { + public string UserId { get; set; } + public string Id { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/OrganizationAutoEnrollStatusResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/OrganizationAutoEnrollStatusResponse.cs new file mode 100644 index 000000000..d354f8ae5 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/OrganizationAutoEnrollStatusResponse.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Models.Response +{ + public class OrganizationAutoEnrollStatusResponse + { + public string Id { get; set; } + public bool ResetPasswordEnabled { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/OrganizationDomainSsoDetailsResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/OrganizationDomainSsoDetailsResponse.cs new file mode 100644 index 000000000..a79640197 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/OrganizationDomainSsoDetailsResponse.cs @@ -0,0 +1,13 @@ +using System; +namespace Bit.Core.Models.Response +{ + public class OrganizationDomainSsoDetailsResponse + { + public bool SsoAvailable { get; set; } + public string DomainName { get; set; } + public string OrganizationIdentifier { get; set; } + public bool SsoRequired { get; set; } + public DateTime? VerifiedDate { get; set; } + } +} + diff --git a/src/Maui/Bitwarden/Core/Models/Response/OrganizationKeysResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/OrganizationKeysResponse.cs new file mode 100644 index 000000000..1f9c7aac5 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/OrganizationKeysResponse.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Models.Response +{ + public class OrganizationKeysResponse + { + public string PrivateKey { get; set; } + public string PublicKey { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/PasswordHistoryResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/PasswordHistoryResponse.cs new file mode 100644 index 000000000..2903ede72 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/PasswordHistoryResponse.cs @@ -0,0 +1,10 @@ +using System; + +namespace Bit.Core.Models.Response +{ + public class PasswordHistoryResponse + { + public string Password { get; set; } + public DateTime? LastUsedDate { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/PasswordlessLoginResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/PasswordlessLoginResponse.cs new file mode 100644 index 000000000..551bc6439 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/PasswordlessLoginResponse.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using Bit.Core.Enums; +using Bit.Core.Models.Data; + +namespace Bit.Core.Models.Response +{ + public class PasswordlessLoginResponse + { + public string Id { get; set; } + public string PublicKey { get; set; } + public string RequestDeviceType { get; set; } + public string RequestIpAddress { get; set; } + public string FingerprintPhrase { get; set; } + public string Key { get; set; } + public string MasterPasswordHash { get; set; } + public DateTime CreationDate { get; set; } + public DateTime? ResponseDate { get; set; } + public bool? RequestApproved { get; set; } + public string Origin { get; set; } + public string RequestAccessCode { get; set; } + public Tuple RequestKeyPair { get; set; } + + public bool IsAnswered => RequestApproved != null && ResponseDate != null; + + public bool IsExpired => CreationDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) < DateTime.UtcNow; + } + + public class PasswordlessLoginsResponse + { + public List Data { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/PolicyResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/PolicyResponse.cs new file mode 100644 index 000000000..d4afbd987 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/PolicyResponse.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Response +{ + public class PolicyResponse + { + public string Id { get; set; } + public string OrganizationId { get; set; } + public PolicyType Type { get; set; } + public Dictionary Data { get; set; } + public bool Enabled { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/PreloginResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/PreloginResponse.cs new file mode 100644 index 000000000..74cfda0ce --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/PreloginResponse.cs @@ -0,0 +1,15 @@ +using Bit.Core.Enums; +using Newtonsoft.Json; + +namespace Bit.Core.Models.Response +{ + public class PreloginResponse + { + public KdfType Kdf { get; set; } + public int KdfIterations { get; set; } + public int? KdfMemory { get; set; } + public int? KdfParallelism { get; set; } + [JsonIgnore] + public KdfConfig KdfConfig => new KdfConfig(Kdf, KdfIterations, KdfMemory, KdfParallelism); + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/ProfileOrganizationResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/ProfileOrganizationResponse.cs new file mode 100644 index 000000000..75d68f6f9 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/ProfileOrganizationResponse.cs @@ -0,0 +1,32 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Data; + +namespace Bit.Core.Models.Response +{ + public class ProfileOrganizationResponse + { + public string Id { get; set; } + public string Name { get; set; } + public bool UseGroups { get; set; } + public bool UseDirectory { get; set; } + public bool UseEvents { get; set; } + public bool UseTotp { get; set; } + public bool Use2fa { get; set; } + public bool UseApi { get; set; } + public bool UsePolicies { get; set; } + public bool UsersGetPremium { get; set; } + public bool SelfHost { get; set; } + public int? Seats { get; set; } + public short? MaxCollections { get; set; } + public short? MaxStorageGb { get; set; } + public string Key { get; set; } + public OrganizationUserStatusType Status { get; set; } + public OrganizationUserType Type { get; set; } + public bool Enabled { get; set; } + public Permissions Permissions { get; set; } = new Permissions(); + public string Identifier { get; set; } + public bool UsesKeyConnector { get; set; } + public string KeyConnectorUrl { get; set; } + + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/ProfileResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/ProfileResponse.cs new file mode 100644 index 000000000..9abc4584e --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/ProfileResponse.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace Bit.Core.Models.Response +{ + public class ProfileResponse + { + public string Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public bool EmailVerified { get; set; } + public bool Premium { get; set; } + public string MasterPasswordHint { get; set; } + public string Culture { get; set; } + public bool TwoFactorEnabled { get; set; } + public string Key { get; set; } + public string PrivateKey { get; set; } + public string SecurityStamp { get; set; } + public bool ForcePasswordReset { get; set; } + public List Organizations { get; set; } + public bool UsesKeyConnector { get; set; } + public string AvatarColor { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/SendFileUploadDataResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/SendFileUploadDataResponse.cs new file mode 100644 index 000000000..8ea79ae0e --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/SendFileUploadDataResponse.cs @@ -0,0 +1,12 @@ +using System; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Response +{ + public class SendFileUploadDataResponse + { + public string Url { get; set; } + public FileUploadType FileUploadType { get; set; } + public SendResponse SendResponse { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/SendResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/SendResponse.cs new file mode 100644 index 000000000..89ee96598 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/SendResponse.cs @@ -0,0 +1,26 @@ +using System; +using Bit.Core.Enums; +using Bit.Core.Models.Api; + +namespace Bit.Core.Models.Response +{ + public class SendResponse + { + public string Id { get; set; } + public string AccessId { get; set; } + public SendType Type { get; set; } + public string Name { get; set; } + public string Notes { get; set; } + public SendFileApi File { get; set; } + public SendTextApi Text { get; set; } + public string Key { get; set; } + public int? MaxAccessCount { get; set; } + public int AccessCount { get; set; } + public DateTime RevisionDate { get; set; } + public DateTime? ExpirationDate { get; set; } + public DateTime DeletionDate { get; set; } + public string Password { get; set; } + public bool Disabled { get; set; } + public bool? HideEmail { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/SsoPrevalidateResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/SsoPrevalidateResponse.cs new file mode 100644 index 000000000..589867a4b --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/SsoPrevalidateResponse.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Response +{ + public class SsoPrevalidateResponse + { + public string Token { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/SyncResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/SyncResponse.cs new file mode 100644 index 000000000..2442367bb --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/SyncResponse.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Response +{ + public class SyncResponse + { + public ProfileResponse Profile { get; set; } + public List Folders { get; set; } = new List(); + public List Collections { get; set; } = new List(); + public List Ciphers { get; set; } = new List(); + public DomainsResponse Domains { get; set; } + public List Policies { get; set; } = new List(); + public List Sends { get; set; } = new List(); + } +} diff --git a/src/Maui/Bitwarden/Core/Models/Response/VerifyMasterPasswordResponse.cs b/src/Maui/Bitwarden/Core/Models/Response/VerifyMasterPasswordResponse.cs new file mode 100644 index 000000000..1a02bc940 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/Response/VerifyMasterPasswordResponse.cs @@ -0,0 +1,9 @@ +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.Response +{ + public class VerifyMasterPasswordResponse + { + public MasterPasswordPolicyOptions MasterPasswordPolicy { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/AccountView.cs b/src/Maui/Bitwarden/Core/Models/View/AccountView.cs new file mode 100644 index 000000000..7224d51bd --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/AccountView.cs @@ -0,0 +1,43 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; + +namespace Bit.Core.Models.View +{ + public class AccountView : View + { + public AccountView() { } + + public AccountView(Account a = null, bool isActive = false) + { + if (a == null) + { + // null will render as "Add Account" row + return; + } + IsAccount = true; + IsActive = isActive; + UserId = a.Profile?.UserId; + Email = a.Profile?.Email; + Name = a.Profile?.Name; + AvatarColor = a.Profile?.AvatarColor; + if (!string.IsNullOrWhiteSpace(a.Settings?.EnvironmentUrls?.WebVault)) + { + Hostname = CoreHelpers.GetHostname(a.Settings?.EnvironmentUrls?.WebVault); + } + else if (!string.IsNullOrWhiteSpace(a.Settings?.EnvironmentUrls?.Base)) + { + Hostname = CoreHelpers.GetHostname(a.Settings?.EnvironmentUrls?.Base); + } + } + + public bool IsAccount { get; set; } + public AuthenticationStatus? AuthStatus { get; set; } + public bool IsActive { get; set; } + public string UserId { get; set; } + public string Email { get; set; } + public string Name { get; set; } + public string Hostname { get; set; } + public string AvatarColor { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/AttachmentView.cs b/src/Maui/Bitwarden/Core/Models/View/AttachmentView.cs new file mode 100644 index 000000000..879ec583c --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/AttachmentView.cs @@ -0,0 +1,36 @@ +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class AttachmentView : View + { + public AttachmentView() { } + + public AttachmentView(Attachment a) + { + Id = a.Id; + Url = a.Url; + Size = a.Size; + SizeName = a.SizeName; + } + + public string Id { get; set; } + public string Url { get; set; } + public string Size { get; set; } + public string SizeName { get; set; } + public string FileName { get; set; } + public SymmetricCryptoKey Key { get; set; } + + public long FileSize + { + get + { + if (!string.IsNullOrWhiteSpace(Size) && long.TryParse(Size, out var s)) + { + return s; + } + return 0; + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/CardView.cs b/src/Maui/Bitwarden/Core/Models/View/CardView.cs new file mode 100644 index 000000000..6b88d01e3 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/CardView.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class CardView : ItemView + { + private string _brand; + private string _number; + private string _subTitle; + + public CardView() { } + + public CardView(Card c) { } + + public string CardholderName { get; set; } + public string ExpMonth { get; set; } + public string ExpYear { get; set; } + public string Code { get; set; } + public string MaskedCode => Code != null ? new string('•', Code.Length) : null; + public string MaskedNumber => Number != null ? new string('•', Number.Length) : null; + + public string Brand + { + get => _brand; + set + { + _brand = value; + _subTitle = null; + } + } + + public string Number + { + get => _number; + set + { + _number = value; + _subTitle = null; + } + } + + public override string SubTitle + { + get + { + if (_subTitle == null) + { + _subTitle = Brand; + if (Number != null && Number.Length >= 4) + { + if (!string.IsNullOrWhiteSpace(_subTitle)) + { + _subTitle += ", "; + } + else + { + _subTitle = string.Empty; + } + // Show last 5 on amex, last 4 for all others + var count = Number.Length >= 5 && Regex.Match(Number, "^3[47]").Success ? 5 : 4; + _subTitle += ("*" + Number.Substring(Number.Length - count)); + } + } + return _subTitle; + } + } + + public string Expiration + { + get + { + var expMonthNull = string.IsNullOrWhiteSpace(ExpMonth); + var expYearNull = string.IsNullOrWhiteSpace(ExpYear); + if (expMonthNull && expYearNull) + { + return null; + } + var expMo = !expMonthNull ? ExpMonth.PadLeft(2, '0') : "__"; + var expYr = !expYearNull ? FormatYear(ExpYear) : "____"; + return string.Format("{0} / {1}", expMo, expYr); + } + } + + public override List> LinkedFieldOptions + { + get => new List>() + { + new KeyValuePair("CardholderName", LinkedIdType.Card_CardholderName), + new KeyValuePair("ExpirationMonth", LinkedIdType.Card_ExpMonth), + new KeyValuePair("ExpirationYear", LinkedIdType.Card_ExpYear), + new KeyValuePair("SecurityCode", LinkedIdType.Card_Code), + new KeyValuePair("Brand", LinkedIdType.Card_Brand), + new KeyValuePair("Number", LinkedIdType.Card_Number), + }; + } + + private string FormatYear(string year) + { + return year.Length == 2 ? string.Concat("20", year) : year; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/CipherView.cs b/src/Maui/Bitwarden/Core/Models/View/CipherView.cs new file mode 100644 index 000000000..8e2202847 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/CipherView.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class CipherView : View, ILaunchableView + { + public CipherView() { } + + public CipherView(Cipher c) + { + Id = c.Id; + OrganizationId = c.OrganizationId; + FolderId = c.FolderId; + Favorite = c.Favorite; + OrganizationUseTotp = c.OrganizationUseTotp; + Edit = c.Edit; + ViewPassword = c.ViewPassword; + Type = c.Type; + LocalData = c.LocalData; + CollectionIds = c.CollectionIds; + RevisionDate = c.RevisionDate; + CreationDate = c.CreationDate; + DeletedDate = c.DeletedDate; + Reprompt = c.Reprompt; + } + + public string Id { get; set; } + public string OrganizationId { get; set; } + public string FolderId { get; set; } + public string Name { get; set; } + public string Notes { get; set; } + public CipherType Type { get; set; } + public bool Favorite { get; set; } + public bool OrganizationUseTotp { get; set; } + public bool Edit { get; set; } + public bool ViewPassword { get; set; } = true; + public Dictionary LocalData { get; set; } + public LoginView Login { get; set; } + public IdentityView Identity { get; set; } + public CardView Card { get; set; } + public SecureNoteView SecureNote { get; set; } + public Fido2KeyView Fido2Key { get; set; } + public List Attachments { get; set; } + public List Fields { get; set; } + public List PasswordHistory { get; set; } + public HashSet CollectionIds { get; set; } + public DateTime RevisionDate { get; set; } + public DateTime CreationDate { get; set; } + public DateTime? DeletedDate { get; set; } + public CipherRepromptType Reprompt { get; set; } + + public ItemView Item + { + get + { + switch (Type) + { + case CipherType.Login: + return Login; + case CipherType.SecureNote: + return SecureNote; + case CipherType.Card: + return Card; + case CipherType.Identity: + return Identity; + case CipherType.Fido2Key: + return Fido2Key; + default: + break; + } + return null; + } + } + + public List> LinkedFieldOptions => Item.LinkedFieldOptions; + public string SubTitle => Item.SubTitle; + public bool Shared => OrganizationId != null; + public bool HasPasswordHistory => PasswordHistory?.Any() ?? false; + public bool HasAttachments => Attachments?.Any() ?? false; + public bool HasOldAttachments + { + get + { + if (HasAttachments) + { + return Attachments.Any(a => a.Key == null); + } + return false; + } + } + public bool HasFields => Fields?.Any() ?? false; + public DateTime? PasswordRevisionDisplayDate + { + get + { + if (Type != CipherType.Login || Login == null) + { + return null; + } + else if (string.IsNullOrWhiteSpace(Login.Password)) + { + return null; + } + return Login.PasswordRevisionDate; + } + } + public bool IsDeleted => DeletedDate.HasValue; + + public string LinkedFieldI18nKey(LinkedIdType id) + { + return LinkedFieldOptions.Find(lfo => lfo.Value == id).Key; + } + + public string ComparableName => Name + Login?.Username + Fido2Key?.UserName; + + public bool CanLaunch => Login?.CanLaunch == true || Fido2Key?.CanLaunch == true; + + public string LaunchUri => Login?.LaunchUri ?? Fido2Key?.LaunchUri; + + public bool IsClonable => OrganizationId is null && Type != CipherType.Fido2Key; + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/CollectionView.cs b/src/Maui/Bitwarden/Core/Models/View/CollectionView.cs new file mode 100644 index 000000000..23251a642 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/CollectionView.cs @@ -0,0 +1,23 @@ +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class CollectionView : View, ITreeNodeObject + { + public CollectionView() { } + + public CollectionView(Collection c) + { + Id = c.Id; + OrganizationId = c.OrganizationId; + ReadOnly = c.ReadOnly; + ExternalId = c.ExternalId; + } + + public string Id { get; set; } + public string OrganizationId { get; set; } + public string Name { get; set; } + public string ExternalId { get; set; } + public bool ReadOnly { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/Fido2KeyView.cs b/src/Maui/Bitwarden/Core/Models/View/Fido2KeyView.cs new file mode 100644 index 000000000..dcdddd8a8 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/Fido2KeyView.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Bit.Core.Enums; + +namespace Bit.Core.Models.View +{ + public class Fido2KeyView : ItemView, ILaunchableView + { + public string NonDiscoverableId { get; set; } + public string KeyType { get; set; } = Constants.DefaultFido2KeyType; + public string KeyAlgorithm { get; set; } = Constants.DefaultFido2KeyAlgorithm; + public string KeyCurve { get; set; } = Constants.DefaultFido2KeyCurve; + public string KeyValue { get; set; } + public string RpId { get; set; } + public string RpName { get; set; } + public string UserHandle { get; set; } + public string UserName { get; set; } + public string Counter { get; set; } + + public override string SubTitle => UserName; + public override List> LinkedFieldOptions => new List>(); + public bool CanLaunch => !string.IsNullOrEmpty(RpId); + public string LaunchUri => $"https://{RpId}"; + + public bool IsUniqueAgainst(Fido2KeyView fido2View) => fido2View?.RpId != RpId || fido2View?.UserName != UserName; + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/FieldView.cs b/src/Maui/Bitwarden/Core/Models/View/FieldView.cs new file mode 100644 index 000000000..e9d74c13a --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/FieldView.cs @@ -0,0 +1,24 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class FieldView : View + { + public FieldView() { } + + public FieldView(Field f) + { + Type = f.Type; + LinkedId = f.LinkedId; + } + + public string Name { get; set; } + public string Value { get; set; } + public FieldType Type { get; set; } + public string MaskedValue => Value != null ? "••••••••" : null; + public bool NewField { get; set; } + public LinkedIdType? LinkedId { get; set; } + public bool BoolValue => bool.TryParse(Value, out var b) && b; + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/FolderView.cs b/src/Maui/Bitwarden/Core/Models/View/FolderView.cs new file mode 100644 index 000000000..f20f91107 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/FolderView.cs @@ -0,0 +1,20 @@ +using System; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class FolderView : View, ITreeNodeObject + { + public FolderView() { } + + public FolderView(Folder f) + { + Id = f.Id; + RevisionDate = f.RevisionDate; + } + + public string Id { get; set; } + public string Name { get; set; } + public DateTime RevisionDate { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/ILaunchableView.cs b/src/Maui/Bitwarden/Core/Models/View/ILaunchableView.cs new file mode 100644 index 000000000..2156d3140 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/ILaunchableView.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Models.View +{ + public interface ILaunchableView + { + bool CanLaunch { get; } + string LaunchUri { get; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/IdentityView.cs b/src/Maui/Bitwarden/Core/Models/View/IdentityView.cs new file mode 100644 index 000000000..b21a5b927 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/IdentityView.cs @@ -0,0 +1,173 @@ +using System.Collections.Generic; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class IdentityView : ItemView + { + private string _firstName; + private string _lastName; + private string _subTitle; + + public IdentityView() { } + + public IdentityView(Identity i) { } + + public string Title { get; set; } + public string FirstName + { + get => _firstName; + set + { + _firstName = value; + _subTitle = null; + } + } + public string MiddleName { get; set; } + public string LastName + { + get => _lastName; + set + { + _lastName = value; + _subTitle = null; + } + } + public string Address1 { get; set; } + public string Address2 { get; set; } + public string Address3 { get; set; } + public string City { get; set; } + public string State { get; set; } + public string PostalCode { get; set; } + public string Country { get; set; } + public string Company { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public string SSN { get; set; } + public string Username { get; set; } + public string PassportNumber { get; set; } + public string LicenseNumber { get; set; } + + public override string SubTitle + { + get + { + if (_subTitle == null && (FirstName != null || LastName != null)) + { + _subTitle = string.Empty; + if (FirstName != null) + { + _subTitle = FirstName; + } + if (LastName != null) + { + if (_subTitle != string.Empty) + { + _subTitle += " "; + } + _subTitle += LastName; + } + } + return _subTitle; + } + } + + public string FullName + { + get + { + if (!string.IsNullOrWhiteSpace(Title) || !string.IsNullOrWhiteSpace(FirstName) || + !string.IsNullOrWhiteSpace(MiddleName) || !string.IsNullOrWhiteSpace(LastName)) + { + var name = string.Empty; + if (!string.IsNullOrWhiteSpace(Title)) + { + name = string.Concat(name, Title, " "); + } + if (!string.IsNullOrWhiteSpace(FirstName)) + { + name = string.Concat(name, FirstName, " "); + } + if (!string.IsNullOrWhiteSpace(MiddleName)) + { + name = string.Concat(name, MiddleName, " "); + } + if (!string.IsNullOrWhiteSpace(LastName)) + { + name = string.Concat(name, LastName); + } + return name.Trim(); + } + return null; + } + } + + public string FullAddress + { + get + { + var address = Address1; + if (!string.IsNullOrWhiteSpace(Address2)) + { + if (!string.IsNullOrWhiteSpace(address)) + { + address += ", "; + } + address += Address2; + } + if (!string.IsNullOrWhiteSpace(Address3)) + { + if (!string.IsNullOrWhiteSpace(address)) + { + address += ", "; + } + address += Address3; + } + return address; + } + } + + public string FullAddressPart2 + { + get + { + if (string.IsNullOrWhiteSpace(City) && string.IsNullOrWhiteSpace(State) && + string.IsNullOrWhiteSpace(PostalCode)) + { + return null; + } + var city = string.IsNullOrWhiteSpace(City) ? "-" : City; + var state = string.IsNullOrWhiteSpace(State) ? "-" : State; + var postalCode = string.IsNullOrWhiteSpace(PostalCode) ? "-" : PostalCode; + return string.Format("{0}, {1}, {2}", city, state, postalCode); + } + } + + public override List> LinkedFieldOptions + { + get => new List>() + { + new KeyValuePair("Title", LinkedIdType.Identity_Title), + new KeyValuePair("MiddleName", LinkedIdType.Identity_MiddleName), + new KeyValuePair("Address1", LinkedIdType.Identity_Address1), + new KeyValuePair("Address2", LinkedIdType.Identity_Address2), + new KeyValuePair("Address3", LinkedIdType.Identity_Address3), + new KeyValuePair("CityTown", LinkedIdType.Identity_City), + new KeyValuePair("StateProvince", LinkedIdType.Identity_State), + new KeyValuePair("ZipPostalCode", LinkedIdType.Identity_PostalCode), + new KeyValuePair("Country", LinkedIdType.Identity_Country), + new KeyValuePair("Company", LinkedIdType.Identity_Company), + new KeyValuePair("Email", LinkedIdType.Identity_Email), + new KeyValuePair("Phone", LinkedIdType.Identity_Phone), + new KeyValuePair("SSN", LinkedIdType.Identity_Ssn), + new KeyValuePair("Username", LinkedIdType.Identity_Username), + new KeyValuePair("PassportNumber", LinkedIdType.Identity_PassportNumber), + new KeyValuePair("LicenseNumber", LinkedIdType.Identity_LicenseNumber), + new KeyValuePair("FirstName", LinkedIdType.Identity_FirstName), + new KeyValuePair("LastName", LinkedIdType.Identity_LastName), + new KeyValuePair("FullName", LinkedIdType.Identity_FullName), + }; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/ItemView.cs b/src/Maui/Bitwarden/Core/Models/View/ItemView.cs new file mode 100644 index 000000000..a264cf441 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/ItemView.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Bit.Core.Enums; + +namespace Bit.Core.Models.View +{ + public abstract class ItemView : View + { + public ItemView() { } + + public abstract string SubTitle { get; } + + public abstract List> LinkedFieldOptions { get; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/LoginUriView.cs b/src/Maui/Bitwarden/Core/Models/View/LoginUriView.cs new file mode 100644 index 000000000..44874ab1c --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/LoginUriView.cs @@ -0,0 +1,113 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; + +namespace Bit.Core.Models.View +{ + public class LoginUriView : View, ILaunchableView + { + private HashSet _canLaunchWhitelist = new HashSet + { + "https://", + "http://", + "ssh://", + "ftp://", + "sftp://", + "irc://", + "vnc://", + "chrome://", + "iosapp://", + "androidapp://", + }; + + private string _uri; + private string _domain; + private string _host; + private bool? _canLaunch; + + public LoginUriView() { } + + public LoginUriView(LoginUri u) + { + Match = u.Match; + } + + public UriMatchType? Match { get; set; } + public string Uri + { + get => _uri; + set + { + _uri = value; + _domain = null; + _canLaunch = null; + } + } + + public string Domain + { + get + { + if (_domain == null && Uri != null) + { + _domain = CoreHelpers.GetDomain(Uri); + if (_domain == string.Empty) + { + _domain = null; + } + } + return _domain; + } + } + + public string Host + { + get + { + if (Match == UriMatchType.RegularExpression) + { + return null; + } + if (_host == null && Uri != null) + { + _host = CoreHelpers.GetHost(Uri); + if (_host == string.Empty) + { + _host = null; + } + } + return _host; + } + } + + public string HostOrUri => Host ?? Uri; + + public bool IsWebsite => Uri != null && (Uri.StartsWith("http://") || Uri.StartsWith("https://") || + (Uri.Contains("://") && Regex.IsMatch(Uri, CoreHelpers.TldEndingRegex))); + + public bool CanLaunch + { + get + { + if (_canLaunch != null) + { + return _canLaunch.Value; + } + if (Uri != null && Match != UriMatchType.RegularExpression) + { + var uri = LaunchUri; + _canLaunch = _canLaunchWhitelist.Any(prefix => uri.StartsWith(prefix)); + return _canLaunch.Value; + } + _canLaunch = false; + return _canLaunch.Value; + } + } + + public string LaunchUri => !Uri.Contains("://") && Regex.IsMatch(Uri, CoreHelpers.TldEndingRegex) ? + string.Concat("http://", Uri) : Uri; + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/LoginView.cs b/src/Maui/Bitwarden/Core/Models/View/LoginView.cs new file mode 100644 index 000000000..94a735dc6 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/LoginView.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class LoginView : ItemView + { + public LoginView() { } + + public LoginView(Login l) + { + PasswordRevisionDate = l.PasswordRevisionDate; + } + + public string Username { get; set; } + public string Password { get; set; } + public DateTime? PasswordRevisionDate { get; set; } + public string Totp { get; set; } + public List Uris { get; set; } + public Fido2KeyView Fido2Key { get; set; } + + public string Uri => HasUris ? Uris[0].Uri : null; + public string MaskedPassword => Password != null ? "••••••••" : null; + public override string SubTitle => Username; + public bool CanLaunch => HasUris && Uris.Any(u => u.CanLaunch); + public string LaunchUri => HasUris ? Uris.FirstOrDefault(u => u.CanLaunch)?.LaunchUri : null; + public bool HasUris => (Uris?.Count ?? 0) > 0; + + public override List> LinkedFieldOptions + { + get => new List>() + { + new KeyValuePair("Username", LinkedIdType.Login_Username), + new KeyValuePair("Password", LinkedIdType.Login_Password), + }; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/PasswordHistoryView.cs b/src/Maui/Bitwarden/Core/Models/View/PasswordHistoryView.cs new file mode 100644 index 000000000..0d3472f52 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/PasswordHistoryView.cs @@ -0,0 +1,18 @@ +using System; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class PasswordHistoryView : View + { + public PasswordHistoryView() { } + + public PasswordHistoryView(PasswordHistory ph) + { + LastUsedDate = ph.LastUsedDate; + } + + public string Password { get; set; } + public DateTime LastUsedDate { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/SecureNoteView.cs b/src/Maui/Bitwarden/Core/Models/View/SecureNoteView.cs new file mode 100644 index 000000000..88a02fcbe --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/SecureNoteView.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class SecureNoteView : ItemView + { + public SecureNoteView() { } + + public SecureNoteView(SecureNote n) + { + Type = n.Type; + } + + public SecureNoteType Type { get; set; } + public override string SubTitle => null; + public override List> LinkedFieldOptions => null; + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/SendFileView.cs b/src/Maui/Bitwarden/Core/Models/View/SendFileView.cs new file mode 100644 index 000000000..3b1d20ac9 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/SendFileView.cs @@ -0,0 +1,23 @@ +using System.Dynamic; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class SendFileView : View + { + public SendFileView() : base() { } + + public SendFileView(SendFile file) + { + Id = file.Id; + Size = file.Size; + SizeName = file.SizeName; + } + + public string Id { get; set; } + public string Size { get; set; } + public string SizeName { get; set; } + public string FileName { get; set; } + public int FileSize => int.TryParse(Size ?? "0", out var sizeInt) ? sizeInt : 0; + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/SendTextView.cs b/src/Maui/Bitwarden/Core/Models/View/SendTextView.cs new file mode 100644 index 000000000..2e391a452 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/SendTextView.cs @@ -0,0 +1,17 @@ +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class SendTextView : View + { + public SendTextView() : base() { } + public SendTextView(SendText text) + { + Hidden = text.Hidden; + } + + public string Text { get; set; } = null; + public bool Hidden { get; set; } + public string MaskedText => Text != null ? "••••••••" : null; + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/SendView.cs b/src/Maui/Bitwarden/Core/Models/View/SendView.cs new file mode 100644 index 000000000..259eea531 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/SendView.cs @@ -0,0 +1,51 @@ +using System; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; + +namespace Bit.Core.Models.View +{ + public class SendView : View + { + public SendView() { } + + public SendView(Send send) : base() + { + Id = send.Id; + AccessId = send.AccessId; + Type = send.Type; + MaxAccessCount = send.MaxAccessCount; + AccessCount = send.AccessCount; + RevisionDate = send.RevisionDate; + DeletionDate = send.DeletionDate; + ExpirationDate = send.ExpirationDate; + Disabled = send.Disabled; + Password = send.Password; + HideEmail = send.HideEmail; + } + + public string Id { get; set; } + public string AccessId { get; set; } + public string Name { get; set; } + public string Notes { get; set; } + public byte[] Key { get; set; } + public SymmetricCryptoKey CryptoKey { get; set; } + public SendType Type { get; set; } + public SendTextView Text { get; set; } = new SendTextView(); + public SendFileView File { get; set; } = new SendFileView(); + public int? MaxAccessCount { get; set; } + public int AccessCount { get; set; } + public DateTime RevisionDate { get; set; } + public DateTime DeletionDate { get; set; } + public DateTime? ExpirationDate { get; set; } + public string Password { get; set; } + public bool Disabled { get; set; } + public string UrlB64Key => Key == null ? null : CoreHelpers.Base64UrlEncode(Key); + public bool HasPassword => Password?.Length > 0; + public bool MaxAccessCountReached => MaxAccessCount.HasValue && AccessCount >= MaxAccessCount.Value; + public bool Expired => ExpirationDate.HasValue && ExpirationDate.Value <= DateTime.UtcNow; + public bool PendingDelete => DeletionDate <= DateTime.UtcNow; + public string DisplayDate => DeletionDate.ToLocalTime().ToString("MMM d, yyyy, h:mm tt"); + public bool HideEmail { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/SimpleCipherView.cs b/src/Maui/Bitwarden/Core/Models/View/SimpleCipherView.cs new file mode 100644 index 000000000..cfd078b4a --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/SimpleCipherView.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Linq; +using Bit.Core.Enums; +using MessagePack; + +namespace Bit.Core.Models.View +{ + [MessagePackObject] + public class SimpleCipherView + { + public SimpleCipherView() + { + } + + public SimpleCipherView(CipherView c) + { + Id = c.Id; + Name = c.Name; + Type = c.Type; + if (c.Login != null) + { + Login = new SimpleLoginView + { + Username = c.Login.Username, + Totp = c.Login.Totp, + Uris = c.Login.Uris?.Select(u => new SimpleLoginUriView(u.Uri)).ToList() + }; + } + } + + [Key(0)] + public string Id { get; set; } + [Key(1)] + public string Name { get; set; } + [IgnoreMember] + public CipherType Type { get; set; } // ignoring on serialization for now, given that all are going to be of type Login + [Key(2)] + public SimpleLoginView Login { get; set; } + } + + [MessagePackObject] + public class SimpleLoginView + { + [Key(0)] + public string Username { get; set; } + [Key(1)] + public string Totp { get; set; } + [Key(2)] + public List Uris { get; set; } + } + + [MessagePackObject] + public class SimpleLoginUriView + { + public SimpleLoginUriView() + { + } + + public SimpleLoginUriView(string uri) + { + Uri = uri; + } + + [Key(0)] + public string Uri { get; set; } + } +} + diff --git a/src/Maui/Bitwarden/Core/Models/View/View.cs b/src/Maui/Bitwarden/Core/Models/View/View.cs new file mode 100644 index 000000000..719e9ad74 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/View.cs @@ -0,0 +1,5 @@ +namespace Bit.Core.Models.View +{ + public abstract class View + { } +} diff --git a/src/Maui/Bitwarden/Core/Models/View/WatchDTO.cs b/src/Maui/Bitwarden/Core/Models/View/WatchDTO.cs new file mode 100644 index 000000000..10c901b87 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Models/View/WatchDTO.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using Bit.Core.Enums; +using Bit.Core.Models.View; +using MessagePack; + +namespace Bit.Core.Models +{ + [MessagePackObject] + public class WatchDTO + { + public WatchDTO() + { + } + + public WatchDTO(WatchState state) + { + State = state; + } + + [Key(0)] + public WatchState State { get; private set; } + + [Key(1)] + public List Ciphers { get; set; } + + [Key(2)] + public UserDataDto UserData { get; set; } + + [Key(3)] + public EnvironmentUrlDataDto EnvironmentData { get; set; } + + //public SettingsDataDto SettingsData { get; set; } + + [MessagePackObject] + public class UserDataDto + { + [Key(0)] + public string Id { get; set; } + + [Key(1)] + public string Email { get; set; } + + [Key(2)] + public string Name { get; set; } + } + + [MessagePackObject] + public class EnvironmentUrlDataDto + { + [Key(0)] + public string Base { get; set; } + + [Key(1)] + public string Icons { get; set; } + } + + //public class SettingsDataDto + //{ + // public int? VaultTimeoutInMinutes { get; set; } + + // public VaultTimeoutAction VaultTimeoutAction { get; set; } + //} + } +} diff --git a/src/Maui/Bitwarden/Core/Resources/eff_long_word_list.txt b/src/Maui/Bitwarden/Core/Resources/eff_long_word_list.txt new file mode 100644 index 000000000..33a62036b --- /dev/null +++ b/src/Maui/Bitwarden/Core/Resources/eff_long_word_list.txt @@ -0,0 +1,7776 @@ +abacus +abdomen +abdominal +abide +abiding +ability +ablaze +able +abnormal +abrasion +abrasive +abreast +abridge +abroad +abruptly +absence +absentee +absently +absinthe +absolute +absolve +abstain +abstract +absurd +accent +acclaim +acclimate +accompany +account +accuracy +accurate +accustom +acetone +achiness +aching +acid +acorn +acquaint +acquire +acre +acrobat +acronym +acting +action +activate +activator +active +activism +activist +activity +actress +acts +acutely +acuteness +aeration +aerobics +aerosol +aerospace +afar +affair +affected +affecting +affection +affidavit +affiliate +affirm +affix +afflicted +affluent +afford +affront +aflame +afloat +aflutter +afoot +afraid +afterglow +afterlife +aftermath +aftermost +afternoon +aged +ageless +agency +agenda +agent +aggregate +aghast +agile +agility +aging +agnostic +agonize +agonizing +agony +agreeable +agreeably +agreed +agreeing +agreement +aground +ahead +ahoy +aide +aids +aim +ajar +alabaster +alarm +albatross +album +alfalfa +algebra +algorithm +alias +alibi +alienable +alienate +aliens +alike +alive +alkaline +alkalize +almanac +almighty +almost +aloe +aloft +aloha +alone +alongside +aloof +alphabet +alright +although +altitude +alto +aluminum +alumni +always +amaretto +amaze +amazingly +amber +ambiance +ambiguity +ambiguous +ambition +ambitious +ambulance +ambush +amendable +amendment +amends +amenity +amiable +amicably +amid +amigo +amino +amiss +ammonia +ammonium +amnesty +amniotic +among +amount +amperage +ample +amplifier +amplify +amply +amuck +amulet +amusable +amused +amusement +amuser +amusing +anaconda +anaerobic +anagram +anatomist +anatomy +anchor +anchovy +ancient +android +anemia +anemic +aneurism +anew +angelfish +angelic +anger +angled +angler +angles +angling +angrily +angriness +anguished +angular +animal +animate +animating +animation +animator +anime +animosity +ankle +annex +annotate +announcer +annoying +annually +annuity +anointer +another +answering +antacid +antarctic +anteater +antelope +antennae +anthem +anthill +anthology +antibody +antics +antidote +antihero +antiquely +antiques +antiquity +antirust +antitoxic +antitrust +antiviral +antivirus +antler +antonym +antsy +anvil +anybody +anyhow +anymore +anyone +anyplace +anything +anytime +anyway +anywhere +aorta +apache +apostle +appealing +appear +appease +appeasing +appendage +appendix +appetite +appetizer +applaud +applause +apple +appliance +applicant +applied +apply +appointee +appraisal +appraiser +apprehend +approach +approval +approve +apricot +april +apron +aptitude +aptly +aqua +aqueduct +arbitrary +arbitrate +ardently +area +arena +arguable +arguably +argue +arise +armadillo +armband +armchair +armed +armful +armhole +arming +armless +armoire +armored +armory +armrest +army +aroma +arose +around +arousal +arrange +array +arrest +arrival +arrive +arrogance +arrogant +arson +art +ascend +ascension +ascent +ascertain +ashamed +ashen +ashes +ashy +aside +askew +asleep +asparagus +aspect +aspirate +aspire +aspirin +astonish +astound +astride +astrology +astronaut +astronomy +astute +atlantic +atlas +atom +atonable +atop +atrium +atrocious +atrophy +attach +attain +attempt +attendant +attendee +attention +attentive +attest +attic +attire +attitude +attractor +attribute +atypical +auction +audacious +audacity +audible +audibly +audience +audio +audition +augmented +august +authentic +author +autism +autistic +autograph +automaker +automated +automatic +autopilot +available +avalanche +avatar +avenge +avenging +avenue +average +aversion +avert +aviation +aviator +avid +avoid +await +awaken +award +aware +awhile +awkward +awning +awoke +awry +axis +babble +babbling +babied +baboon +backache +backboard +backboned +backdrop +backed +backer +backfield +backfire +backhand +backing +backlands +backlash +backless +backlight +backlit +backlog +backpack +backpedal +backrest +backroom +backshift +backside +backslid +backspace +backspin +backstab +backstage +backtalk +backtrack +backup +backward +backwash +backwater +backyard +bacon +bacteria +bacterium +badass +badge +badland +badly +badness +baffle +baffling +bagel +bagful +baggage +bagged +baggie +bagginess +bagging +baggy +bagpipe +baguette +baked +bakery +bakeshop +baking +balance +balancing +balcony +balmy +balsamic +bamboo +banana +banish +banister +banjo +bankable +bankbook +banked +banker +banking +banknote +bankroll +banner +bannister +banshee +banter +barbecue +barbed +barbell +barber +barcode +barge +bargraph +barista +baritone +barley +barmaid +barman +barn +barometer +barrack +barracuda +barrel +barrette +barricade +barrier +barstool +bartender +barterer +bash +basically +basics +basil +basin +basis +basket +batboy +batch +bath +baton +bats +battalion +battered +battering +battery +batting +battle +bauble +bazooka +blabber +bladder +blade +blah +blame +blaming +blanching +blandness +blank +blaspheme +blasphemy +blast +blatancy +blatantly +blazer +blazing +bleach +bleak +bleep +blemish +blend +bless +blighted +blimp +bling +blinked +blinker +blinking +blinks +blip +blissful +blitz +blizzard +bloated +bloating +blob +blog +bloomers +blooming +blooper +blot +blouse +blubber +bluff +bluish +blunderer +blunt +blurb +blurred +blurry +blurt +blush +blustery +boaster +boastful +boasting +boat +bobbed +bobbing +bobble +bobcat +bobsled +bobtail +bodacious +body +bogged +boggle +bogus +boil +bok +bolster +bolt +bonanza +bonded +bonding +bondless +boned +bonehead +boneless +bonelike +boney +bonfire +bonnet +bonsai +bonus +bony +boogeyman +boogieman +book +boondocks +booted +booth +bootie +booting +bootlace +bootleg +boots +boozy +borax +boring +borough +borrower +borrowing +boss +botanical +botanist +botany +botch +both +bottle +bottling +bottom +bounce +bouncing +bouncy +bounding +boundless +bountiful +bovine +boxcar +boxer +boxing +boxlike +boxy +breach +breath +breeches +breeching +breeder +breeding +breeze +breezy +brethren +brewery +brewing +briar +bribe +brick +bride +bridged +brigade +bright +brilliant +brim +bring +brink +brisket +briskly +briskness +bristle +brittle +broadband +broadcast +broaden +broadly +broadness +broadside +broadways +broiler +broiling +broken +broker +bronchial +bronco +bronze +bronzing +brook +broom +brought +browbeat +brownnose +browse +browsing +bruising +brunch +brunette +brunt +brush +brussels +brute +brutishly +bubble +bubbling +bubbly +buccaneer +bucked +bucket +buckle +buckshot +buckskin +bucktooth +buckwheat +buddhism +buddhist +budding +buddy +budget +buffalo +buffed +buffer +buffing +buffoon +buggy +bulb +bulge +bulginess +bulgur +bulk +bulldog +bulldozer +bullfight +bullfrog +bullhorn +bullion +bullish +bullpen +bullring +bullseye +bullwhip +bully +bunch +bundle +bungee +bunion +bunkbed +bunkhouse +bunkmate +bunny +bunt +busboy +bush +busily +busload +bust +busybody +buzz +cabana +cabbage +cabbie +cabdriver +cable +caboose +cache +cackle +cacti +cactus +caddie +caddy +cadet +cadillac +cadmium +cage +cahoots +cake +calamari +calamity +calcium +calculate +calculus +caliber +calibrate +calm +caloric +calorie +calzone +camcorder +cameo +camera +camisole +camper +campfire +camping +campsite +campus +canal +canary +cancel +candied +candle +candy +cane +canine +canister +cannabis +canned +canning +cannon +cannot +canola +canon +canopener +canopy +canteen +canyon +capable +capably +capacity +cape +capillary +capital +capitol +capped +capricorn +capsize +capsule +caption +captivate +captive +captivity +capture +caramel +carat +caravan +carbon +cardboard +carded +cardiac +cardigan +cardinal +cardstock +carefully +caregiver +careless +caress +caretaker +cargo +caring +carless +carload +carmaker +carnage +carnation +carnival +carnivore +carol +carpenter +carpentry +carpool +carport +carried +carrot +carrousel +carry +cartel +cartload +carton +cartoon +cartridge +cartwheel +carve +carving +carwash +cascade +case +cash +casing +casino +casket +cassette +casually +casualty +catacomb +catalog +catalyst +catalyze +catapult +cataract +catatonic +catcall +catchable +catcher +catching +catchy +caterer +catering +catfight +catfish +cathedral +cathouse +catlike +catnap +catnip +catsup +cattail +cattishly +cattle +catty +catwalk +caucasian +caucus +causal +causation +cause +causing +cauterize +caution +cautious +cavalier +cavalry +caviar +cavity +cedar +celery +celestial +celibacy +celibate +celtic +cement +census +ceramics +ceremony +certainly +certainty +certified +certify +cesarean +cesspool +chafe +chaffing +chain +chair +chalice +challenge +chamber +chamomile +champion +chance +change +channel +chant +chaos +chaperone +chaplain +chapped +chaps +chapter +character +charbroil +charcoal +charger +charging +chariot +charity +charm +charred +charter +charting +chase +chasing +chaste +chastise +chastity +chatroom +chatter +chatting +chatty +cheating +cheddar +cheek +cheer +cheese +cheesy +chef +chemicals +chemist +chemo +cherisher +cherub +chess +chest +chevron +chevy +chewable +chewer +chewing +chewy +chief +chihuahua +childcare +childhood +childish +childless +childlike +chili +chill +chimp +chip +chirping +chirpy +chitchat +chivalry +chive +chloride +chlorine +choice +chokehold +choking +chomp +chooser +choosing +choosy +chop +chosen +chowder +chowtime +chrome +chubby +chuck +chug +chummy +chump +chunk +churn +chute +cider +cilantro +cinch +cinema +cinnamon +circle +circling +circular +circulate +circus +citable +citadel +citation +citizen +citric +citrus +city +civic +civil +clad +claim +clambake +clammy +clamor +clamp +clamshell +clang +clanking +clapped +clapper +clapping +clarify +clarinet +clarity +clash +clasp +class +clatter +clause +clavicle +claw +clay +clean +clear +cleat +cleaver +cleft +clench +clergyman +clerical +clerk +clever +clicker +client +climate +climatic +cling +clinic +clinking +clip +clique +cloak +clobber +clock +clone +cloning +closable +closure +clothes +clothing +cloud +clover +clubbed +clubbing +clubhouse +clump +clumsily +clumsy +clunky +clustered +clutch +clutter +coach +coagulant +coastal +coaster +coasting +coastland +coastline +coat +coauthor +cobalt +cobbler +cobweb +cocoa +coconut +cod +coeditor +coerce +coexist +coffee +cofounder +cognition +cognitive +cogwheel +coherence +coherent +cohesive +coil +coke +cola +cold +coleslaw +coliseum +collage +collapse +collar +collected +collector +collide +collie +collision +colonial +colonist +colonize +colony +colossal +colt +coma +come +comfort +comfy +comic +coming +comma +commence +commend +comment +commerce +commode +commodity +commodore +common +commotion +commute +commuting +compacted +compacter +compactly +compactor +companion +company +compare +compel +compile +comply +component +composed +composer +composite +compost +composure +compound +compress +comprised +computer +computing +comrade +concave +conceal +conceded +concept +concerned +concert +conch +concierge +concise +conclude +concrete +concur +condense +condiment +condition +condone +conducive +conductor +conduit +cone +confess +confetti +confidant +confident +confider +confiding +configure +confined +confining +confirm +conflict +conform +confound +confront +confused +confusing +confusion +congenial +congested +congrats +congress +conical +conjoined +conjure +conjuror +connected +connector +consensus +consent +console +consoling +consonant +constable +constant +constrain +constrict +construct +consult +consumer +consuming +contact +container +contempt +contend +contented +contently +contents +contest +context +contort +contour +contrite +control +contusion +convene +convent +copartner +cope +copied +copier +copilot +coping +copious +copper +copy +coral +cork +cornball +cornbread +corncob +cornea +corned +corner +cornfield +cornflake +cornhusk +cornmeal +cornstalk +corny +coronary +coroner +corporal +corporate +corral +correct +corridor +corrode +corroding +corrosive +corsage +corset +cortex +cosigner +cosmetics +cosmic +cosmos +cosponsor +cost +cottage +cotton +couch +cough +could +countable +countdown +counting +countless +country +county +courier +covenant +cover +coveted +coveting +coyness +cozily +coziness +cozy +crabbing +crabgrass +crablike +crabmeat +cradle +cradling +crafter +craftily +craftsman +craftwork +crafty +cramp +cranberry +crane +cranial +cranium +crank +crate +crave +craving +crawfish +crawlers +crawling +crayfish +crayon +crazed +crazily +craziness +crazy +creamed +creamer +creamlike +crease +creasing +creatable +create +creation +creative +creature +credible +credibly +credit +creed +creme +creole +crepe +crept +crescent +crested +cresting +crestless +crevice +crewless +crewman +crewmate +crib +cricket +cried +crier +crimp +crimson +cringe +cringing +crinkle +crinkly +crisped +crisping +crisply +crispness +crispy +criteria +critter +croak +crock +crook +croon +crop +cross +crouch +crouton +crowbar +crowd +crown +crucial +crudely +crudeness +cruelly +cruelness +cruelty +crumb +crummiest +crummy +crumpet +crumpled +cruncher +crunching +crunchy +crusader +crushable +crushed +crusher +crushing +crust +crux +crying +cryptic +crystal +cubbyhole +cube +cubical +cubicle +cucumber +cuddle +cuddly +cufflink +culinary +culminate +culpable +culprit +cultivate +cultural +culture +cupbearer +cupcake +cupid +cupped +cupping +curable +curator +curdle +cure +curfew +curing +curled +curler +curliness +curling +curly +curry +curse +cursive +cursor +curtain +curtly +curtsy +curvature +curve +curvy +cushy +cusp +cussed +custard +custodian +custody +customary +customer +customize +customs +cut +cycle +cyclic +cycling +cyclist +cylinder +cymbal +cytoplasm +cytoplast +dab +dad +daffodil +dagger +daily +daintily +dainty +dairy +daisy +dallying +dance +dancing +dandelion +dander +dandruff +dandy +danger +dangle +dangling +daredevil +dares +daringly +darkened +darkening +darkish +darkness +darkroom +darling +darn +dart +darwinism +dash +dastardly +data +datebook +dating +daughter +daunting +dawdler +dawn +daybed +daybreak +daycare +daydream +daylight +daylong +dayroom +daytime +dazzler +dazzling +deacon +deafening +deafness +dealer +dealing +dealmaker +dealt +dean +debatable +debate +debating +debit +debrief +debtless +debtor +debug +debunk +decade +decaf +decal +decathlon +decay +deceased +deceit +deceiver +deceiving +december +decency +decent +deception +deceptive +decibel +decidable +decimal +decimeter +decipher +deck +declared +decline +decode +decompose +decorated +decorator +decoy +decrease +decree +dedicate +dedicator +deduce +deduct +deed +deem +deepen +deeply +deepness +deface +defacing +defame +default +defeat +defection +defective +defendant +defender +defense +defensive +deferral +deferred +defiance +defiant +defile +defiling +define +definite +deflate +deflation +deflator +deflected +deflector +defog +deforest +defraud +defrost +deftly +defuse +defy +degraded +degrading +degrease +degree +dehydrate +deity +dejected +delay +delegate +delegator +delete +deletion +delicacy +delicate +delicious +delighted +delirious +delirium +deliverer +delivery +delouse +delta +deluge +delusion +deluxe +demanding +demeaning +demeanor +demise +democracy +democrat +demote +demotion +demystify +denatured +deniable +denial +denim +denote +dense +density +dental +dentist +denture +deny +deodorant +deodorize +departed +departure +depict +deplete +depletion +deplored +deploy +deport +depose +depraved +depravity +deprecate +depress +deprive +depth +deputize +deputy +derail +deranged +derby +derived +desecrate +deserve +deserving +designate +designed +designer +designing +deskbound +desktop +deskwork +desolate +despair +despise +despite +destiny +destitute +destruct +detached +detail +detection +detective +detector +detention +detergent +detest +detonate +detonator +detoxify +detract +deuce +devalue +deviancy +deviant +deviate +deviation +deviator +device +devious +devotedly +devotee +devotion +devourer +devouring +devoutly +dexterity +dexterous +diabetes +diabetic +diabolic +diagnoses +diagnosis +diagram +dial +diameter +diaper +diaphragm +diary +dice +dicing +dictate +dictation +dictator +difficult +diffused +diffuser +diffusion +diffusive +dig +dilation +diligence +diligent +dill +dilute +dime +diminish +dimly +dimmed +dimmer +dimness +dimple +diner +dingbat +dinghy +dinginess +dingo +dingy +dining +dinner +diocese +dioxide +diploma +dipped +dipper +dipping +directed +direction +directive +directly +directory +direness +dirtiness +disabled +disagree +disallow +disarm +disarray +disaster +disband +disbelief +disburse +discard +discern +discharge +disclose +discolor +discount +discourse +discover +discuss +disdain +disengage +disfigure +disgrace +dish +disinfect +disjoin +disk +dislike +disliking +dislocate +dislodge +disloyal +dismantle +dismay +dismiss +dismount +disobey +disorder +disown +disparate +disparity +dispatch +dispense +dispersal +dispersed +disperser +displace +display +displease +disposal +dispose +disprove +dispute +disregard +disrupt +dissuade +distance +distant +distaste +distill +distinct +distort +distract +distress +district +distrust +ditch +ditto +ditzy +dividable +divided +dividend +dividers +dividing +divinely +diving +divinity +divisible +divisibly +division +divisive +divorcee +dizziness +dizzy +doable +docile +dock +doctrine +document +dodge +dodgy +doily +doing +dole +dollar +dollhouse +dollop +dolly +dolphin +domain +domelike +domestic +dominion +dominoes +donated +donation +donator +donor +donut +doodle +doorbell +doorframe +doorknob +doorman +doormat +doornail +doorpost +doorstep +doorstop +doorway +doozy +dork +dormitory +dorsal +dosage +dose +dotted +doubling +douche +dove +down +dowry +doze +drab +dragging +dragonfly +dragonish +dragster +drainable +drainage +drained +drainer +drainpipe +dramatic +dramatize +drank +drapery +drastic +draw +dreaded +dreadful +dreadlock +dreamboat +dreamily +dreamland +dreamless +dreamlike +dreamt +dreamy +drearily +dreary +drench +dress +drew +dribble +dried +drier +drift +driller +drilling +drinkable +drinking +dripping +drippy +drivable +driven +driver +driveway +driving +drizzle +drizzly +drone +drool +droop +drop-down +dropbox +dropkick +droplet +dropout +dropper +drove +drown +drowsily +drudge +drum +dry +dubbed +dubiously +duchess +duckbill +ducking +duckling +ducktail +ducky +duct +dude +duffel +dugout +duh +duke +duller +dullness +duly +dumping +dumpling +dumpster +duo +dupe +duplex +duplicate +duplicity +durable +durably +duration +duress +during +dusk +dust +dutiful +duty +duvet +dwarf +dweeb +dwelled +dweller +dwelling +dwindle +dwindling +dynamic +dynamite +dynasty +dyslexia +dyslexic +each +eagle +earache +eardrum +earflap +earful +earlobe +early +earmark +earmuff +earphone +earpiece +earplugs +earring +earshot +earthen +earthlike +earthling +earthly +earthworm +earthy +earwig +easeful +easel +easiest +easily +easiness +easing +eastbound +eastcoast +easter +eastward +eatable +eaten +eatery +eating +eats +ebay +ebony +ebook +ecard +eccentric +echo +eclair +eclipse +ecologist +ecology +economic +economist +economy +ecosphere +ecosystem +edge +edginess +edging +edgy +edition +editor +educated +education +educator +eel +effective +effects +efficient +effort +eggbeater +egging +eggnog +eggplant +eggshell +egomaniac +egotism +egotistic +either +eject +elaborate +elastic +elated +elbow +eldercare +elderly +eldest +electable +election +elective +elephant +elevate +elevating +elevation +elevator +eleven +elf +eligible +eligibly +eliminate +elite +elitism +elixir +elk +ellipse +elliptic +elm +elongated +elope +eloquence +eloquent +elsewhere +elude +elusive +elves +email +embargo +embark +embassy +embattled +embellish +ember +embezzle +emblaze +emblem +embody +embolism +emboss +embroider +emcee +emerald +emergency +emission +emit +emote +emoticon +emotion +empathic +empathy +emperor +emphases +emphasis +emphasize +emphatic +empirical +employed +employee +employer +emporium +empower +emptier +emptiness +empty +emu +enable +enactment +enamel +enchanted +enchilada +encircle +enclose +enclosure +encode +encore +encounter +encourage +encroach +encrust +encrypt +endanger +endeared +endearing +ended +ending +endless +endnote +endocrine +endorphin +endorse +endowment +endpoint +endurable +endurance +enduring +energetic +energize +energy +enforced +enforcer +engaged +engaging +engine +engorge +engraved +engraver +engraving +engross +engulf +enhance +enigmatic +enjoyable +enjoyably +enjoyer +enjoying +enjoyment +enlarged +enlarging +enlighten +enlisted +enquirer +enrage +enrich +enroll +enslave +ensnare +ensure +entail +entangled +entering +entertain +enticing +entire +entitle +entity +entomb +entourage +entrap +entree +entrench +entrust +entryway +entwine +enunciate +envelope +enviable +enviably +envious +envision +envoy +envy +enzyme +epic +epidemic +epidermal +epidermis +epidural +epilepsy +epileptic +epilogue +epiphany +episode +equal +equate +equation +equator +equinox +equipment +equity +equivocal +eradicate +erasable +erased +eraser +erasure +ergonomic +errand +errant +erratic +error +erupt +escalate +escalator +escapable +escapade +escapist +escargot +eskimo +esophagus +espionage +espresso +esquire +essay +essence +essential +establish +estate +esteemed +estimate +estimator +estranged +estrogen +etching +eternal +eternity +ethanol +ether +ethically +ethics +euphemism +evacuate +evacuee +evade +evaluate +evaluator +evaporate +evasion +evasive +even +everglade +evergreen +everybody +everyday +everyone +evict +evidence +evident +evil +evoke +evolution +evolve +exact +exalted +example +excavate +excavator +exceeding +exception +excess +exchange +excitable +exciting +exclaim +exclude +excluding +exclusion +exclusive +excretion +excretory +excursion +excusable +excusably +excuse +exemplary +exemplify +exemption +exerciser +exert +exes +exfoliate +exhale +exhaust +exhume +exile +existing +exit +exodus +exonerate +exorcism +exorcist +expand +expanse +expansion +expansive +expectant +expedited +expediter +expel +expend +expenses +expensive +expert +expire +expiring +explain +expletive +explicit +explode +exploit +explore +exploring +exponent +exporter +exposable +expose +exposure +express +expulsion +exquisite +extended +extending +extent +extenuate +exterior +external +extinct +extortion +extradite +extras +extrovert +extrude +extruding +exuberant +fable +fabric +fabulous +facebook +facecloth +facedown +faceless +facelift +faceplate +faceted +facial +facility +facing +facsimile +faction +factoid +factor +factsheet +factual +faculty +fade +fading +failing +falcon +fall +false +falsify +fame +familiar +family +famine +famished +fanatic +fancied +fanciness +fancy +fanfare +fang +fanning +fantasize +fantastic +fantasy +fascism +fastball +faster +fasting +fastness +faucet +favorable +favorably +favored +favoring +favorite +fax +feast +federal +fedora +feeble +feed +feel +feisty +feline +felt-tip +feminine +feminism +feminist +feminize +femur +fence +fencing +fender +ferment +fernlike +ferocious +ferocity +ferret +ferris +ferry +fervor +fester +festival +festive +festivity +fetal +fetch +fever +fiber +fiction +fiddle +fiddling +fidelity +fidgeting +fidgety +fifteen +fifth +fiftieth +fifty +figment +figure +figurine +filing +filled +filler +filling +film +filter +filth +filtrate +finale +finalist +finalize +finally +finance +financial +finch +fineness +finer +finicky +finished +finisher +finishing +finite +finless +finlike +fiscally +fit +five +flaccid +flagman +flagpole +flagship +flagstick +flagstone +flail +flakily +flaky +flame +flammable +flanked +flanking +flannels +flap +flaring +flashback +flashbulb +flashcard +flashily +flashing +flashy +flask +flatbed +flatfoot +flatly +flatness +flatten +flattered +flatterer +flattery +flattop +flatware +flatworm +flavored +flavorful +flavoring +flaxseed +fled +fleshed +fleshy +flick +flier +flight +flinch +fling +flint +flip +flirt +float +flock +flogging +flop +floral +florist +floss +flounder +flyable +flyaway +flyer +flying +flyover +flypaper +foam +foe +fog +foil +folic +folk +follicle +follow +fondling +fondly +fondness +fondue +font +food +fool +footage +football +footbath +footboard +footer +footgear +foothill +foothold +footing +footless +footman +footnote +footpad +footpath +footprint +footrest +footsie +footsore +footwear +footwork +fossil +foster +founder +founding +fountain +fox +foyer +fraction +fracture +fragile +fragility +fragment +fragrance +fragrant +frail +frame +framing +frantic +fraternal +frayed +fraying +frays +freckled +freckles +freebase +freebee +freebie +freedom +freefall +freehand +freeing +freeload +freely +freemason +freeness +freestyle +freeware +freeway +freewill +freezable +freezing +freight +french +frenzied +frenzy +frequency +frequent +fresh +fretful +fretted +friction +friday +fridge +fried +friend +frighten +frightful +frigidity +frigidly +frill +fringe +frisbee +frisk +fritter +frivolous +frolic +from +front +frostbite +frosted +frostily +frosting +frostlike +frosty +froth +frown +frozen +fructose +frugality +frugally +fruit +frustrate +frying +gab +gaffe +gag +gainfully +gaining +gains +gala +gallantly +galleria +gallery +galley +gallon +gallows +gallstone +galore +galvanize +gambling +game +gaming +gamma +gander +gangly +gangrene +gangway +gap +garage +garbage +garden +gargle +garland +garlic +garment +garnet +garnish +garter +gas +gatherer +gathering +gating +gauging +gauntlet +gauze +gave +gawk +gazing +gear +gecko +geek +geiger +gem +gender +generic +generous +genetics +genre +gentile +gentleman +gently +gents +geography +geologic +geologist +geology +geometric +geometry +geranium +gerbil +geriatric +germicide +germinate +germless +germproof +gestate +gestation +gesture +getaway +getting +getup +giant +gibberish +giblet +giddily +giddiness +giddy +gift +gigabyte +gigahertz +gigantic +giggle +giggling +giggly +gigolo +gilled +gills +gimmick +girdle +giveaway +given +giver +giving +gizmo +gizzard +glacial +glacier +glade +gladiator +gladly +glamorous +glamour +glance +glancing +glandular +glare +glaring +glass +glaucoma +glazing +gleaming +gleeful +glider +gliding +glimmer +glimpse +glisten +glitch +glitter +glitzy +gloater +gloating +gloomily +gloomy +glorified +glorifier +glorify +glorious +glory +gloss +glove +glowing +glowworm +glucose +glue +gluten +glutinous +glutton +gnarly +gnat +goal +goatskin +goes +goggles +going +goldfish +goldmine +goldsmith +golf +goliath +gonad +gondola +gone +gong +good +gooey +goofball +goofiness +goofy +google +goon +gopher +gore +gorged +gorgeous +gory +gosling +gossip +gothic +gotten +gout +gown +grab +graceful +graceless +gracious +gradation +graded +grader +gradient +grading +gradually +graduate +graffiti +grafted +grafting +grain +granddad +grandkid +grandly +grandma +grandpa +grandson +granite +granny +granola +grant +granular +grape +graph +grapple +grappling +grasp +grass +gratified +gratify +grating +gratitude +gratuity +gravel +graveness +graves +graveyard +gravitate +gravity +gravy +gray +grazing +greasily +greedily +greedless +greedy +green +greeter +greeting +grew +greyhound +grid +grief +grievance +grieving +grievous +grill +grimace +grimacing +grime +griminess +grimy +grinch +grinning +grip +gristle +grit +groggily +groggy +groin +groom +groove +grooving +groovy +grope +ground +grouped +grout +grove +grower +growing +growl +grub +grudge +grudging +grueling +gruffly +grumble +grumbling +grumbly +grumpily +grunge +grunt +guacamole +guidable +guidance +guide +guiding +guileless +guise +gulf +gullible +gully +gulp +gumball +gumdrop +gumminess +gumming +gummy +gurgle +gurgling +guru +gush +gusto +gusty +gutless +guts +gutter +guy +guzzler +gyration +habitable +habitant +habitat +habitual +hacked +hacker +hacking +hacksaw +had +haggler +haiku +half +halogen +halt +halved +halves +hamburger +hamlet +hammock +hamper +hamster +hamstring +handbag +handball +handbook +handbrake +handcart +handclap +handclasp +handcraft +handcuff +handed +handful +handgrip +handgun +handheld +handiness +handiwork +handlebar +handled +handler +handling +handmade +handoff +handpick +handprint +handrail +handsaw +handset +handsfree +handshake +handstand +handwash +handwork +handwoven +handwrite +handyman +hangnail +hangout +hangover +hangup +hankering +hankie +hanky +haphazard +happening +happier +happiest +happily +happiness +happy +harbor +hardcopy +hardcore +hardcover +harddisk +hardened +hardener +hardening +hardhat +hardhead +hardiness +hardly +hardness +hardship +hardware +hardwired +hardwood +hardy +harmful +harmless +harmonica +harmonics +harmonize +harmony +harness +harpist +harsh +harvest +hash +hassle +haste +hastily +hastiness +hasty +hatbox +hatchback +hatchery +hatchet +hatching +hatchling +hate +hatless +hatred +haunt +haven +hazard +hazelnut +hazily +haziness +hazing +hazy +headache +headband +headboard +headcount +headdress +headed +header +headfirst +headgear +heading +headlamp +headless +headlock +headphone +headpiece +headrest +headroom +headscarf +headset +headsman +headstand +headstone +headway +headwear +heap +heat +heave +heavily +heaviness +heaving +hedge +hedging +heftiness +hefty +helium +helmet +helper +helpful +helping +helpless +helpline +hemlock +hemstitch +hence +henchman +henna +herald +herbal +herbicide +herbs +heritage +hermit +heroics +heroism +herring +herself +hertz +hesitancy +hesitant +hesitate +hexagon +hexagram +hubcap +huddle +huddling +huff +hug +hula +hulk +hull +human +humble +humbling +humbly +humid +humiliate +humility +humming +hummus +humongous +humorist +humorless +humorous +humpback +humped +humvee +hunchback +hundredth +hunger +hungrily +hungry +hunk +hunter +hunting +huntress +huntsman +hurdle +hurled +hurler +hurling +hurray +hurricane +hurried +hurry +hurt +husband +hush +husked +huskiness +hut +hybrid +hydrant +hydrated +hydration +hydrogen +hydroxide +hyperlink +hypertext +hyphen +hypnoses +hypnosis +hypnotic +hypnotism +hypnotist +hypnotize +hypocrisy +hypocrite +ibuprofen +ice +iciness +icing +icky +icon +icy +idealism +idealist +idealize +ideally +idealness +identical +identify +identity +ideology +idiocy +idiom +idly +igloo +ignition +ignore +iguana +illicitly +illusion +illusive +image +imaginary +imagines +imaging +imbecile +imitate +imitation +immature +immerse +immersion +imminent +immobile +immodest +immorally +immortal +immovable +immovably +immunity +immunize +impaired +impale +impart +impatient +impeach +impeding +impending +imperfect +imperial +impish +implant +implement +implicate +implicit +implode +implosion +implosive +imply +impolite +important +importer +impose +imposing +impotence +impotency +impotent +impound +imprecise +imprint +imprison +impromptu +improper +improve +improving +improvise +imprudent +impulse +impulsive +impure +impurity +iodine +iodize +ion +ipad +iphone +ipod +irate +irk +iron +irregular +irrigate +irritable +irritably +irritant +irritate +islamic +islamist +isolated +isolating +isolation +isotope +issue +issuing +italicize +italics +item +itinerary +itunes +ivory +ivy +jab +jackal +jacket +jackknife +jackpot +jailbird +jailbreak +jailer +jailhouse +jalapeno +jam +janitor +january +jargon +jarring +jasmine +jaundice +jaunt +java +jawed +jawless +jawline +jaws +jaybird +jaywalker +jazz +jeep +jeeringly +jellied +jelly +jersey +jester +jet +jiffy +jigsaw +jimmy +jingle +jingling +jinx +jitters +jittery +job +jockey +jockstrap +jogger +jogging +john +joining +jokester +jokingly +jolliness +jolly +jolt +jot +jovial +joyfully +joylessly +joyous +joyride +joystick +jubilance +jubilant +judge +judgingly +judicial +judiciary +judo +juggle +juggling +jugular +juice +juiciness +juicy +jujitsu +jukebox +july +jumble +jumbo +jump +junction +juncture +june +junior +juniper +junkie +junkman +junkyard +jurist +juror +jury +justice +justifier +justify +justly +justness +juvenile +kabob +kangaroo +karaoke +karate +karma +kebab +keenly +keenness +keep +keg +kelp +kennel +kept +kerchief +kerosene +kettle +kick +kiln +kilobyte +kilogram +kilometer +kilowatt +kilt +kimono +kindle +kindling +kindly +kindness +kindred +kinetic +kinfolk +king +kinship +kinsman +kinswoman +kissable +kisser +kissing +kitchen +kite +kitten +kitty +kiwi +kleenex +knapsack +knee +knelt +knickers +knoll +koala +kooky +kosher +krypton +kudos +kung +labored +laborer +laboring +laborious +labrador +ladder +ladies +ladle +ladybug +ladylike +lagged +lagging +lagoon +lair +lake +lance +landed +landfall +landfill +landing +landlady +landless +landline +landlord +landmark +landmass +landmine +landowner +landscape +landside +landslide +language +lankiness +lanky +lantern +lapdog +lapel +lapped +lapping +laptop +lard +large +lark +lash +lasso +last +latch +late +lather +latitude +latrine +latter +latticed +launch +launder +laundry +laurel +lavender +lavish +laxative +lazily +laziness +lazy +lecturer +left +legacy +legal +legend +legged +leggings +legible +legibly +legislate +lego +legroom +legume +legwarmer +legwork +lemon +lend +length +lens +lent +leotard +lesser +letdown +lethargic +lethargy +letter +lettuce +level +leverage +levers +levitate +levitator +liability +liable +liberty +librarian +library +licking +licorice +lid +life +lifter +lifting +liftoff +ligament +likely +likeness +likewise +liking +lilac +lilly +lily +limb +limeade +limelight +limes +limit +limping +limpness +line +lingo +linguini +linguist +lining +linked +linoleum +linseed +lint +lion +lip +liquefy +liqueur +liquid +lisp +list +litigate +litigator +litmus +litter +little +livable +lived +lively +liver +livestock +lividly +living +lizard +lubricant +lubricate +lucid +luckily +luckiness +luckless +lucrative +ludicrous +lugged +lukewarm +lullaby +lumber +luminance +luminous +lumpiness +lumping +lumpish +lunacy +lunar +lunchbox +luncheon +lunchroom +lunchtime +lung +lurch +lure +luridness +lurk +lushly +lushness +luster +lustfully +lustily +lustiness +lustrous +lusty +luxurious +luxury +lying +lyrically +lyricism +lyricist +lyrics +macarena +macaroni +macaw +mace +machine +machinist +magazine +magenta +maggot +magical +magician +magma +magnesium +magnetic +magnetism +magnetize +magnifier +magnify +magnitude +magnolia +mahogany +maimed +majestic +majesty +majorette +majority +makeover +maker +makeshift +making +malformed +malt +mama +mammal +mammary +mammogram +manager +managing +manatee +mandarin +mandate +mandatory +mandolin +manger +mangle +mango +mangy +manhandle +manhole +manhood +manhunt +manicotti +manicure +manifesto +manila +mankind +manlike +manliness +manly +manmade +manned +mannish +manor +manpower +mantis +mantra +manual +many +map +marathon +marauding +marbled +marbles +marbling +march +mardi +margarine +margarita +margin +marigold +marina +marine +marital +maritime +marlin +marmalade +maroon +married +marrow +marry +marshland +marshy +marsupial +marvelous +marxism +mascot +masculine +mashed +mashing +massager +masses +massive +mastiff +matador +matchbook +matchbox +matcher +matching +matchless +material +maternal +maternity +math +mating +matriarch +matrimony +matrix +matron +matted +matter +maturely +maturing +maturity +mauve +maverick +maximize +maximum +maybe +mayday +mayflower +moaner +moaning +mobile +mobility +mobilize +mobster +mocha +mocker +mockup +modified +modify +modular +modulator +module +moisten +moistness +moisture +molar +molasses +mold +molecular +molecule +molehill +mollusk +mom +monastery +monday +monetary +monetize +moneybags +moneyless +moneywise +mongoose +mongrel +monitor +monkhood +monogamy +monogram +monologue +monopoly +monorail +monotone +monotype +monoxide +monsieur +monsoon +monstrous +monthly +monument +moocher +moodiness +moody +mooing +moonbeam +mooned +moonlight +moonlike +moonlit +moonrise +moonscape +moonshine +moonstone +moonwalk +mop +morale +morality +morally +morbidity +morbidly +morphine +morphing +morse +mortality +mortally +mortician +mortified +mortify +mortuary +mosaic +mossy +most +mothball +mothproof +motion +motivate +motivator +motive +motocross +motor +motto +mountable +mountain +mounted +mounting +mourner +mournful +mouse +mousiness +moustache +mousy +mouth +movable +move +movie +moving +mower +mowing +much +muck +mud +mug +mulberry +mulch +mule +mulled +mullets +multiple +multiply +multitask +multitude +mumble +mumbling +mumbo +mummified +mummify +mummy +mumps +munchkin +mundane +municipal +muppet +mural +murkiness +murky +murmuring +muscular +museum +mushily +mushiness +mushroom +mushy +music +musket +muskiness +musky +mustang +mustard +muster +mustiness +musty +mutable +mutate +mutation +mute +mutilated +mutilator +mutiny +mutt +mutual +muzzle +myself +myspace +mystified +mystify +myth +nacho +nag +nail +name +naming +nanny +nanometer +nape +napkin +napped +napping +nappy +narrow +nastily +nastiness +national +native +nativity +natural +nature +naturist +nautical +navigate +navigator +navy +nearby +nearest +nearly +nearness +neatly +neatness +nebula +nebulizer +nectar +negate +negation +negative +neglector +negligee +negligent +negotiate +nemeses +nemesis +neon +nephew +nerd +nervous +nervy +nest +net +neurology +neuron +neurosis +neurotic +neuter +neutron +never +next +nibble +nickname +nicotine +niece +nifty +nimble +nimbly +nineteen +ninetieth +ninja +nintendo +ninth +nuclear +nuclei +nucleus +nugget +nullify +number +numbing +numbly +numbness +numeral +numerate +numerator +numeric +numerous +nuptials +nursery +nursing +nurture +nutcase +nutlike +nutmeg +nutrient +nutshell +nuttiness +nutty +nuzzle +nylon +oaf +oak +oasis +oat +obedience +obedient +obituary +object +obligate +obliged +oblivion +oblivious +oblong +obnoxious +oboe +obscure +obscurity +observant +observer +observing +obsessed +obsession +obsessive +obsolete +obstacle +obstinate +obstruct +obtain +obtrusive +obtuse +obvious +occultist +occupancy +occupant +occupier +occupy +ocean +ocelot +octagon +octane +october +octopus +ogle +oil +oink +ointment +okay +old +olive +olympics +omega +omen +ominous +omission +omit +omnivore +onboard +oncoming +ongoing +onion +online +onlooker +only +onscreen +onset +onshore +onslaught +onstage +onto +onward +onyx +oops +ooze +oozy +opacity +opal +open +operable +operate +operating +operation +operative +operator +opium +opossum +opponent +oppose +opposing +opposite +oppressed +oppressor +opt +opulently +osmosis +other +otter +ouch +ought +ounce +outage +outback +outbid +outboard +outbound +outbreak +outburst +outcast +outclass +outcome +outdated +outdoors +outer +outfield +outfit +outflank +outgoing +outgrow +outhouse +outing +outlast +outlet +outline +outlook +outlying +outmatch +outmost +outnumber +outplayed +outpost +outpour +output +outrage +outrank +outreach +outright +outscore +outsell +outshine +outshoot +outsider +outskirts +outsmart +outsource +outspoken +outtakes +outthink +outward +outweigh +outwit +oval +ovary +oven +overact +overall +overarch +overbid +overbill +overbite +overblown +overboard +overbook +overbuilt +overcast +overcoat +overcome +overcook +overcrowd +overdraft +overdrawn +overdress +overdrive +overdue +overeager +overeater +overexert +overfed +overfeed +overfill +overflow +overfull +overgrown +overhand +overhang +overhaul +overhead +overhear +overheat +overhung +overjoyed +overkill +overlabor +overlaid +overlap +overlay +overload +overlook +overlord +overlying +overnight +overpass +overpay +overplant +overplay +overpower +overprice +overrate +overreach +overreact +override +overripe +overrule +overrun +overshoot +overshot +oversight +oversized +oversleep +oversold +overspend +overstate +overstay +overstep +overstock +overstuff +oversweet +overtake +overthrow +overtime +overtly +overtone +overture +overturn +overuse +overvalue +overview +overwrite +owl +oxford +oxidant +oxidation +oxidize +oxidizing +oxygen +oxymoron +oyster +ozone +paced +pacemaker +pacific +pacifier +pacifism +pacifist +pacify +padded +padding +paddle +paddling +padlock +pagan +pager +paging +pajamas +palace +palatable +palm +palpable +palpitate +paltry +pampered +pamperer +pampers +pamphlet +panama +pancake +pancreas +panda +pandemic +pang +panhandle +panic +panning +panorama +panoramic +panther +pantomime +pantry +pants +pantyhose +paparazzi +papaya +paper +paprika +papyrus +parabola +parachute +parade +paradox +paragraph +parakeet +paralegal +paralyses +paralysis +paralyze +paramedic +parameter +paramount +parasail +parasite +parasitic +parcel +parched +parchment +pardon +parish +parka +parking +parkway +parlor +parmesan +parole +parrot +parsley +parsnip +partake +parted +parting +partition +partly +partner +partridge +party +passable +passably +passage +passcode +passenger +passerby +passing +passion +passive +passivism +passover +passport +password +pasta +pasted +pastel +pastime +pastor +pastrami +pasture +pasty +patchwork +patchy +paternal +paternity +path +patience +patient +patio +patriarch +patriot +patrol +patronage +patronize +pauper +pavement +paver +pavestone +pavilion +paving +pawing +payable +payback +paycheck +payday +payee +payer +paying +payment +payphone +payroll +pebble +pebbly +pecan +pectin +peculiar +peddling +pediatric +pedicure +pedigree +pedometer +pegboard +pelican +pellet +pelt +pelvis +penalize +penalty +pencil +pendant +pending +penholder +penknife +pennant +penniless +penny +penpal +pension +pentagon +pentagram +pep +perceive +percent +perch +percolate +perennial +perfected +perfectly +perfume +periscope +perish +perjurer +perjury +perkiness +perky +perm +peroxide +perpetual +perplexed +persecute +persevere +persuaded +persuader +pesky +peso +pessimism +pessimist +pester +pesticide +petal +petite +petition +petri +petroleum +petted +petticoat +pettiness +petty +petunia +phantom +phobia +phoenix +phonebook +phoney +phonics +phoniness +phony +phosphate +photo +phrase +phrasing +placard +placate +placidly +plank +planner +plant +plasma +plaster +plastic +plated +platform +plating +platinum +platonic +platter +platypus +plausible +plausibly +playable +playback +player +playful +playgroup +playhouse +playing +playlist +playmaker +playmate +playoff +playpen +playroom +playset +plaything +playtime +plaza +pleading +pleat +pledge +plentiful +plenty +plethora +plexiglas +pliable +plod +plop +plot +plow +ploy +pluck +plug +plunder +plunging +plural +plus +plutonium +plywood +poach +pod +poem +poet +pogo +pointed +pointer +pointing +pointless +pointy +poise +poison +poker +poking +polar +police +policy +polio +polish +politely +polka +polo +polyester +polygon +polygraph +polymer +poncho +pond +pony +popcorn +pope +poplar +popper +poppy +popsicle +populace +popular +populate +porcupine +pork +porous +porridge +portable +portal +portfolio +porthole +portion +portly +portside +poser +posh +posing +possible +possibly +possum +postage +postal +postbox +postcard +posted +poster +posting +postnasal +posture +postwar +pouch +pounce +pouncing +pound +pouring +pout +powdered +powdering +powdery +power +powwow +pox +praising +prance +prancing +pranker +prankish +prankster +prayer +praying +preacher +preaching +preachy +preamble +precinct +precise +precision +precook +precut +predator +predefine +predict +preface +prefix +preflight +preformed +pregame +pregnancy +pregnant +preheated +prelaunch +prelaw +prelude +premiere +premises +premium +prenatal +preoccupy +preorder +prepaid +prepay +preplan +preppy +preschool +prescribe +preseason +preset +preshow +president +presoak +press +presume +presuming +preteen +pretended +pretender +pretense +pretext +pretty +pretzel +prevail +prevalent +prevent +preview +previous +prewar +prewashed +prideful +pried +primal +primarily +primary +primate +primer +primp +princess +print +prior +prism +prison +prissy +pristine +privacy +private +privatize +prize +proactive +probable +probably +probation +probe +probing +probiotic +problem +procedure +process +proclaim +procreate +procurer +prodigal +prodigy +produce +product +profane +profanity +professed +professor +profile +profound +profusely +progeny +prognosis +program +progress +projector +prologue +prolonged +promenade +prominent +promoter +promotion +prompter +promptly +prone +prong +pronounce +pronto +proofing +proofread +proofs +propeller +properly +property +proponent +proposal +propose +props +prorate +protector +protegee +proton +prototype +protozoan +protract +protrude +proud +provable +proved +proven +provided +provider +providing +province +proving +provoke +provoking +provolone +prowess +prowler +prowling +proximity +proxy +prozac +prude +prudishly +prune +pruning +pry +psychic +public +publisher +pucker +pueblo +pug +pull +pulmonary +pulp +pulsate +pulse +pulverize +puma +pumice +pummel +punch +punctual +punctuate +punctured +pungent +punisher +punk +pupil +puppet +puppy +purchase +pureblood +purebred +purely +pureness +purgatory +purge +purging +purifier +purify +purist +puritan +purity +purple +purplish +purposely +purr +purse +pursuable +pursuant +pursuit +purveyor +pushcart +pushchair +pusher +pushiness +pushing +pushover +pushpin +pushup +pushy +putdown +putt +puzzle +puzzling +pyramid +pyromania +python +quack +quadrant +quail +quaintly +quake +quaking +qualified +qualifier +qualify +quality +qualm +quantum +quarrel +quarry +quartered +quarterly +quarters +quartet +quench +query +quicken +quickly +quickness +quicksand +quickstep +quiet +quill +quilt +quintet +quintuple +quirk +quit +quiver +quizzical +quotable +quotation +quote +rabid +race +racing +racism +rack +racoon +radar +radial +radiance +radiantly +radiated +radiation +radiator +radio +radish +raffle +raft +rage +ragged +raging +ragweed +raider +railcar +railing +railroad +railway +raisin +rake +raking +rally +ramble +rambling +ramp +ramrod +ranch +rancidity +random +ranged +ranger +ranging +ranked +ranking +ransack +ranting +rants +rare +rarity +rascal +rash +rasping +ravage +raven +ravine +raving +ravioli +ravishing +reabsorb +reach +reacquire +reaction +reactive +reactor +reaffirm +ream +reanalyze +reappear +reapply +reappoint +reapprove +rearrange +rearview +reason +reassign +reassure +reattach +reawake +rebalance +rebate +rebel +rebirth +reboot +reborn +rebound +rebuff +rebuild +rebuilt +reburial +rebuttal +recall +recant +recapture +recast +recede +recent +recess +recharger +recipient +recital +recite +reckless +reclaim +recliner +reclining +recluse +reclusive +recognize +recoil +recollect +recolor +reconcile +reconfirm +reconvene +recopy +record +recount +recoup +recovery +recreate +rectal +rectangle +rectified +rectify +recycled +recycler +recycling +reemerge +reenact +reenter +reentry +reexamine +referable +referee +reference +refill +refinance +refined +refinery +refining +refinish +reflected +reflector +reflex +reflux +refocus +refold +reforest +reformat +reformed +reformer +reformist +refract +refrain +refreeze +refresh +refried +refueling +refund +refurbish +refurnish +refusal +refuse +refusing +refutable +refute +regain +regalia +regally +reggae +regime +region +register +registrar +registry +regress +regretful +regroup +regular +regulate +regulator +rehab +reheat +rehire +rehydrate +reimburse +reissue +reiterate +rejoice +rejoicing +rejoin +rekindle +relapse +relapsing +relatable +related +relation +relative +relax +relay +relearn +release +relenting +reliable +reliably +reliance +reliant +relic +relieve +relieving +relight +relish +relive +reload +relocate +relock +reluctant +rely +remake +remark +remarry +rematch +remedial +remedy +remember +reminder +remindful +remission +remix +remnant +remodeler +remold +remorse +remote +removable +removal +removed +remover +removing +rename +renderer +rendering +rendition +renegade +renewable +renewably +renewal +renewed +renounce +renovate +renovator +rentable +rental +rented +renter +reoccupy +reoccur +reopen +reorder +repackage +repacking +repaint +repair +repave +repaying +repayment +repeal +repeated +repeater +repent +rephrase +replace +replay +replica +reply +reporter +repose +repossess +repost +repressed +reprimand +reprint +reprise +reproach +reprocess +reproduce +reprogram +reps +reptile +reptilian +repugnant +repulsion +repulsive +repurpose +reputable +reputably +request +require +requisite +reroute +rerun +resale +resample +rescuer +reseal +research +reselect +reseller +resemble +resend +resent +reset +reshape +reshoot +reshuffle +residence +residency +resident +residual +residue +resigned +resilient +resistant +resisting +resize +resolute +resolved +resonant +resonate +resort +resource +respect +resubmit +result +resume +resupply +resurface +resurrect +retail +retainer +retaining +retake +retaliate +retention +rethink +retinal +retired +retiree +retiring +retold +retool +retorted +retouch +retrace +retract +retrain +retread +retreat +retrial +retrieval +retriever +retry +return +retying +retype +reunion +reunite +reusable +reuse +reveal +reveler +revenge +revenue +reverb +revered +reverence +reverend +reversal +reverse +reversing +reversion +revert +revisable +revise +revision +revisit +revivable +revival +reviver +reviving +revocable +revoke +revolt +revolver +revolving +reward +rewash +rewind +rewire +reword +rework +rewrap +rewrite +rhyme +ribbon +ribcage +rice +riches +richly +richness +rickety +ricotta +riddance +ridden +ride +riding +rifling +rift +rigging +rigid +rigor +rimless +rimmed +rind +rink +rinse +rinsing +riot +ripcord +ripeness +ripening +ripping +ripple +rippling +riptide +rise +rising +risk +risotto +ritalin +ritzy +rival +riverbank +riverbed +riverboat +riverside +riveter +riveting +roamer +roaming +roast +robbing +robe +robin +robotics +robust +rockband +rocker +rocket +rockfish +rockiness +rocking +rocklike +rockslide +rockstar +rocky +rogue +roman +romp +rope +roping +roster +rosy +rotten +rotting +rotunda +roulette +rounding +roundish +roundness +roundup +roundworm +routine +routing +rover +roving +royal +rubbed +rubber +rubbing +rubble +rubdown +ruby +ruckus +rudder +rug +ruined +rule +rumble +rumbling +rummage +rumor +runaround +rundown +runner +running +runny +runt +runway +rupture +rural +ruse +rush +rust +rut +sabbath +sabotage +sacrament +sacred +sacrifice +sadden +saddlebag +saddled +saddling +sadly +sadness +safari +safeguard +safehouse +safely +safeness +saffron +saga +sage +sagging +saggy +said +saint +sake +salad +salami +salaried +salary +saline +salon +saloon +salsa +salt +salutary +salute +salvage +salvaging +salvation +same +sample +sampling +sanction +sanctity +sanctuary +sandal +sandbag +sandbank +sandbar +sandblast +sandbox +sanded +sandfish +sanding +sandlot +sandpaper +sandpit +sandstone +sandstorm +sandworm +sandy +sanitary +sanitizer +sank +santa +sapling +sappiness +sappy +sarcasm +sarcastic +sardine +sash +sasquatch +sassy +satchel +satiable +satin +satirical +satisfied +satisfy +saturate +saturday +sauciness +saucy +sauna +savage +savanna +saved +savings +savior +savor +saxophone +say +scabbed +scabby +scalded +scalding +scale +scaling +scallion +scallop +scalping +scam +scandal +scanner +scanning +scant +scapegoat +scarce +scarcity +scarecrow +scared +scarf +scarily +scariness +scarring +scary +scavenger +scenic +schedule +schematic +scheme +scheming +schilling +schnapps +scholar +science +scientist +scion +scoff +scolding +scone +scoop +scooter +scope +scorch +scorebook +scorecard +scored +scoreless +scorer +scoring +scorn +scorpion +scotch +scoundrel +scoured +scouring +scouting +scouts +scowling +scrabble +scraggly +scrambled +scrambler +scrap +scratch +scrawny +screen +scribble +scribe +scribing +scrimmage +script +scroll +scrooge +scrounger +scrubbed +scrubber +scruffy +scrunch +scrutiny +scuba +scuff +sculptor +sculpture +scurvy +scuttle +secluded +secluding +seclusion +second +secrecy +secret +sectional +sector +secular +securely +security +sedan +sedate +sedation +sedative +sediment +seduce +seducing +segment +seismic +seizing +seldom +selected +selection +selective +selector +self +seltzer +semantic +semester +semicolon +semifinal +seminar +semisoft +semisweet +senate +senator +send +senior +senorita +sensation +sensitive +sensitize +sensually +sensuous +sepia +september +septic +septum +sequel +sequence +sequester +series +sermon +serotonin +serpent +serrated +serve +service +serving +sesame +sessions +setback +setting +settle +settling +setup +sevenfold +seventeen +seventh +seventy +severity +shabby +shack +shaded +shadily +shadiness +shading +shadow +shady +shaft +shakable +shakily +shakiness +shaking +shaky +shale +shallot +shallow +shame +shampoo +shamrock +shank +shanty +shape +shaping +share +sharpener +sharper +sharpie +sharply +sharpness +shawl +sheath +shed +sheep +sheet +shelf +shell +shelter +shelve +shelving +sherry +shield +shifter +shifting +shiftless +shifty +shimmer +shimmy +shindig +shine +shingle +shininess +shining +shiny +ship +shirt +shivering +shock +shone +shoplift +shopper +shopping +shoptalk +shore +shortage +shortcake +shortcut +shorten +shorter +shorthand +shortlist +shortly +shortness +shorts +shortwave +shorty +shout +shove +showbiz +showcase +showdown +shower +showgirl +showing +showman +shown +showoff +showpiece +showplace +showroom +showy +shrank +shrapnel +shredder +shredding +shrewdly +shriek +shrill +shrimp +shrine +shrink +shrivel +shrouded +shrubbery +shrubs +shrug +shrunk +shucking +shudder +shuffle +shuffling +shun +shush +shut +shy +siamese +siberian +sibling +siding +sierra +siesta +sift +sighing +silenced +silencer +silent +silica +silicon +silk +silliness +silly +silo +silt +silver +similarly +simile +simmering +simple +simplify +simply +sincere +sincerity +singer +singing +single +singular +sinister +sinless +sinner +sinuous +sip +siren +sister +sitcom +sitter +sitting +situated +situation +sixfold +sixteen +sixth +sixties +sixtieth +sixtyfold +sizable +sizably +size +sizing +sizzle +sizzling +skater +skating +skedaddle +skeletal +skeleton +skeptic +sketch +skewed +skewer +skid +skied +skier +skies +skiing +skilled +skillet +skillful +skimmed +skimmer +skimming +skimpily +skincare +skinhead +skinless +skinning +skinny +skintight +skipper +skipping +skirmish +skirt +skittle +skydiver +skylight +skyline +skype +skyrocket +skyward +slab +slacked +slacker +slacking +slackness +slacks +slain +slam +slander +slang +slapping +slapstick +slashed +slashing +slate +slather +slaw +sled +sleek +sleep +sleet +sleeve +slept +sliceable +sliced +slicer +slicing +slick +slider +slideshow +sliding +slighted +slighting +slightly +slimness +slimy +slinging +slingshot +slinky +slip +slit +sliver +slobbery +slogan +sloped +sloping +sloppily +sloppy +slot +slouching +slouchy +sludge +slug +slum +slurp +slush +sly +small +smartly +smartness +smasher +smashing +smashup +smell +smelting +smile +smilingly +smirk +smite +smith +smitten +smock +smog +smoked +smokeless +smokiness +smoking +smoky +smolder +smooth +smother +smudge +smudgy +smuggler +smuggling +smugly +smugness +snack +snagged +snaking +snap +snare +snarl +snazzy +sneak +sneer +sneeze +sneezing +snide +sniff +snippet +snipping +snitch +snooper +snooze +snore +snoring +snorkel +snort +snout +snowbird +snowboard +snowbound +snowcap +snowdrift +snowdrop +snowfall +snowfield +snowflake +snowiness +snowless +snowman +snowplow +snowshoe +snowstorm +snowsuit +snowy +snub +snuff +snuggle +snugly +snugness +speak +spearfish +spearhead +spearman +spearmint +species +specimen +specked +speckled +specks +spectacle +spectator +spectrum +speculate +speech +speed +spellbind +speller +spelling +spendable +spender +spending +spent +spew +sphere +spherical +sphinx +spider +spied +spiffy +spill +spilt +spinach +spinal +spindle +spinner +spinning +spinout +spinster +spiny +spiral +spirited +spiritism +spirits +spiritual +splashed +splashing +splashy +splatter +spleen +splendid +splendor +splice +splicing +splinter +splotchy +splurge +spoilage +spoiled +spoiler +spoiling +spoils +spoken +spokesman +sponge +spongy +sponsor +spoof +spookily +spooky +spool +spoon +spore +sporting +sports +sporty +spotless +spotlight +spotted +spotter +spotting +spotty +spousal +spouse +spout +sprain +sprang +sprawl +spray +spree +sprig +spring +sprinkled +sprinkler +sprint +sprite +sprout +spruce +sprung +spry +spud +spur +sputter +spyglass +squabble +squad +squall +squander +squash +squatted +squatter +squatting +squeak +squealer +squealing +squeamish +squeegee +squeeze +squeezing +squid +squiggle +squiggly +squint +squire +squirt +squishier +squishy +stability +stabilize +stable +stack +stadium +staff +stage +staging +stagnant +stagnate +stainable +stained +staining +stainless +stalemate +staleness +stalling +stallion +stamina +stammer +stamp +stand +stank +staple +stapling +starboard +starch +stardom +stardust +starfish +stargazer +staring +stark +starless +starlet +starlight +starlit +starring +starry +starship +starter +starting +startle +startling +startup +starved +starving +stash +state +static +statistic +statue +stature +status +statute +statutory +staunch +stays +steadfast +steadier +steadily +steadying +steam +steed +steep +steerable +steering +steersman +stegosaur +stellar +stem +stench +stencil +step +stereo +sterile +sterility +sterilize +sterling +sternness +sternum +stew +stick +stiffen +stiffly +stiffness +stifle +stifling +stillness +stilt +stimulant +stimulate +stimuli +stimulus +stinger +stingily +stinging +stingray +stingy +stinking +stinky +stipend +stipulate +stir +stitch +stock +stoic +stoke +stole +stomp +stonewall +stoneware +stonework +stoning +stony +stood +stooge +stool +stoop +stoplight +stoppable +stoppage +stopped +stopper +stopping +stopwatch +storable +storage +storeroom +storewide +storm +stout +stove +stowaway +stowing +straddle +straggler +strained +strainer +straining +strangely +stranger +strangle +strategic +strategy +stratus +straw +stray +streak +stream +street +strength +strenuous +strep +stress +stretch +strewn +stricken +strict +stride +strife +strike +striking +strive +striving +strobe +strode +stroller +strongbox +strongly +strongman +struck +structure +strudel +struggle +strum +strung +strut +stubbed +stubble +stubbly +stubborn +stucco +stuck +student +studied +studio +study +stuffed +stuffing +stuffy +stumble +stumbling +stump +stung +stunned +stunner +stunning +stunt +stupor +sturdily +sturdy +styling +stylishly +stylist +stylized +stylus +suave +subarctic +subatomic +subdivide +subdued +subduing +subfloor +subgroup +subheader +subject +sublease +sublet +sublevel +sublime +submarine +submerge +submersed +submitter +subpanel +subpar +subplot +subprime +subscribe +subscript +subsector +subside +subsiding +subsidize +subsidy +subsoil +subsonic +substance +subsystem +subtext +subtitle +subtly +subtotal +subtract +subtype +suburb +subway +subwoofer +subzero +succulent +such +suction +sudden +sudoku +suds +sufferer +suffering +suffice +suffix +suffocate +suffrage +sugar +suggest +suing +suitable +suitably +suitcase +suitor +sulfate +sulfide +sulfite +sulfur +sulk +sullen +sulphate +sulphuric +sultry +superbowl +superglue +superhero +superior +superjet +superman +supermom +supernova +supervise +supper +supplier +supply +support +supremacy +supreme +surcharge +surely +sureness +surface +surfacing +surfboard +surfer +surgery +surgical +surging +surname +surpass +surplus +surprise +surreal +surrender +surrogate +surround +survey +survival +survive +surviving +survivor +sushi +suspect +suspend +suspense +sustained +sustainer +swab +swaddling +swagger +swampland +swan +swapping +swarm +sway +swear +sweat +sweep +swell +swept +swerve +swifter +swiftly +swiftness +swimmable +swimmer +swimming +swimsuit +swimwear +swinger +swinging +swipe +swirl +switch +swivel +swizzle +swooned +swoop +swoosh +swore +sworn +swung +sycamore +sympathy +symphonic +symphony +symptom +synapse +syndrome +synergy +synopses +synopsis +synthesis +synthetic +syrup +system +t-shirt +tabasco +tabby +tableful +tables +tablet +tableware +tabloid +tackiness +tacking +tackle +tackling +tacky +taco +tactful +tactical +tactics +tactile +tactless +tadpole +taekwondo +tag +tainted +take +taking +talcum +talisman +tall +talon +tamale +tameness +tamer +tamper +tank +tanned +tannery +tanning +tantrum +tapeless +tapered +tapering +tapestry +tapioca +tapping +taps +tarantula +target +tarmac +tarnish +tarot +tartar +tartly +tartness +task +tassel +taste +tastiness +tasting +tasty +tattered +tattle +tattling +tattoo +taunt +tavern +thank +that +thaw +theater +theatrics +thee +theft +theme +theology +theorize +thermal +thermos +thesaurus +these +thesis +thespian +thicken +thicket +thickness +thieving +thievish +thigh +thimble +thing +think +thinly +thinner +thinness +thinning +thirstily +thirsting +thirsty +thirteen +thirty +thong +thorn +those +thousand +thrash +thread +threaten +threefold +thrift +thrill +thrive +thriving +throat +throbbing +throng +throttle +throwaway +throwback +thrower +throwing +thud +thumb +thumping +thursday +thus +thwarting +thyself +tiara +tibia +tidal +tidbit +tidiness +tidings +tidy +tiger +tighten +tightly +tightness +tightrope +tightwad +tigress +tile +tiling +till +tilt +timid +timing +timothy +tinderbox +tinfoil +tingle +tingling +tingly +tinker +tinkling +tinsel +tinsmith +tint +tinwork +tiny +tipoff +tipped +tipper +tipping +tiptoeing +tiptop +tiring +tissue +trace +tracing +track +traction +tractor +trade +trading +tradition +traffic +tragedy +trailing +trailside +train +traitor +trance +tranquil +transfer +transform +translate +transpire +transport +transpose +trapdoor +trapeze +trapezoid +trapped +trapper +trapping +traps +trash +travel +traverse +travesty +tray +treachery +treading +treadmill +treason +treat +treble +tree +trekker +tremble +trembling +tremor +trench +trend +trespass +triage +trial +triangle +tribesman +tribunal +tribune +tributary +tribute +triceps +trickery +trickily +tricking +trickle +trickster +tricky +tricolor +tricycle +trident +tried +trifle +trifocals +trillion +trilogy +trimester +trimmer +trimming +trimness +trinity +trio +tripod +tripping +triumph +trivial +trodden +trolling +trombone +trophy +tropical +tropics +trouble +troubling +trough +trousers +trout +trowel +truce +truck +truffle +trump +trunks +trustable +trustee +trustful +trusting +trustless +truth +try +tubby +tubeless +tubular +tucking +tuesday +tug +tuition +tulip +tumble +tumbling +tummy +turban +turbine +turbofan +turbojet +turbulent +turf +turkey +turmoil +turret +turtle +tusk +tutor +tutu +tux +tweak +tweed +tweet +tweezers +twelve +twentieth +twenty +twerp +twice +twiddle +twiddling +twig +twilight +twine +twins +twirl +twistable +twisted +twister +twisting +twisty +twitch +twitter +tycoon +tying +tyke +udder +ultimate +ultimatum +ultra +umbilical +umbrella +umpire +unabashed +unable +unadorned +unadvised +unafraid +unaired +unaligned +unaltered +unarmored +unashamed +unaudited +unawake +unaware +unbaked +unbalance +unbeaten +unbend +unbent +unbiased +unbitten +unblended +unblessed +unblock +unbolted +unbounded +unboxed +unbraided +unbridle +unbroken +unbuckled +unbundle +unburned +unbutton +uncanny +uncapped +uncaring +uncertain +unchain +unchanged +uncharted +uncheck +uncivil +unclad +unclaimed +unclamped +unclasp +uncle +unclip +uncloak +unclog +unclothed +uncoated +uncoiled +uncolored +uncombed +uncommon +uncooked +uncork +uncorrupt +uncounted +uncouple +uncouth +uncover +uncross +uncrown +uncrushed +uncured +uncurious +uncurled +uncut +undamaged +undated +undaunted +undead +undecided +undefined +underage +underarm +undercoat +undercook +undercut +underdog +underdone +underfed +underfeed +underfoot +undergo +undergrad +underhand +underline +underling +undermine +undermost +underpaid +underpass +underpay +underrate +undertake +undertone +undertook +undertow +underuse +underwear +underwent +underwire +undesired +undiluted +undivided +undocked +undoing +undone +undrafted +undress +undrilled +undusted +undying +unearned +unearth +unease +uneasily +uneasy +uneatable +uneaten +unedited +unelected +unending +unengaged +unenvied +unequal +unethical +uneven +unexpired +unexposed +unfailing +unfair +unfasten +unfazed +unfeeling +unfiled +unfilled +unfitted +unfitting +unfixable +unfixed +unflawed +unfocused +unfold +unfounded +unframed +unfreeze +unfrosted +unfrozen +unfunded +unglazed +ungloved +unglue +ungodly +ungraded +ungreased +unguarded +unguided +unhappily +unhappy +unharmed +unhealthy +unheard +unhearing +unheated +unhelpful +unhidden +unhinge +unhitched +unholy +unhook +unicorn +unicycle +unified +unifier +uniformed +uniformly +unify +unimpeded +uninjured +uninstall +uninsured +uninvited +union +uniquely +unisexual +unison +unissued +unit +universal +universe +unjustly +unkempt +unkind +unknotted +unknowing +unknown +unlaced +unlatch +unlawful +unleaded +unlearned +unleash +unless +unleveled +unlighted +unlikable +unlimited +unlined +unlinked +unlisted +unlit +unlivable +unloaded +unloader +unlocked +unlocking +unlovable +unloved +unlovely +unloving +unluckily +unlucky +unmade +unmanaged +unmanned +unmapped +unmarked +unmasked +unmasking +unmatched +unmindful +unmixable +unmixed +unmolded +unmoral +unmovable +unmoved +unmoving +unnamable +unnamed +unnatural +unneeded +unnerve +unnerving +unnoticed +unopened +unopposed +unpack +unpadded +unpaid +unpainted +unpaired +unpaved +unpeeled +unpicked +unpiloted +unpinned +unplanned +unplanted +unpleased +unpledged +unplowed +unplug +unpopular +unproven +unquote +unranked +unrated +unraveled +unreached +unread +unreal +unreeling +unrefined +unrelated +unrented +unrest +unretired +unrevised +unrigged +unripe +unrivaled +unroasted +unrobed +unroll +unruffled +unruly +unrushed +unsaddle +unsafe +unsaid +unsalted +unsaved +unsavory +unscathed +unscented +unscrew +unsealed +unseated +unsecured +unseeing +unseemly +unseen +unselect +unselfish +unsent +unsettled +unshackle +unshaken +unshaved +unshaven +unsheathe +unshipped +unsightly +unsigned +unskilled +unsliced +unsmooth +unsnap +unsocial +unsoiled +unsold +unsolved +unsorted +unspoiled +unspoken +unstable +unstaffed +unstamped +unsteady +unsterile +unstirred +unstitch +unstopped +unstuck +unstuffed +unstylish +unsubtle +unsubtly +unsuited +unsure +unsworn +untagged +untainted +untaken +untamed +untangled +untapped +untaxed +unthawed +unthread +untidy +untie +until +untimed +untimely +untitled +untoasted +untold +untouched +untracked +untrained +untreated +untried +untrimmed +untrue +untruth +unturned +untwist +untying +unusable +unused +unusual +unvalued +unvaried +unvarying +unveiled +unveiling +unvented +unviable +unvisited +unvocal +unwanted +unwarlike +unwary +unwashed +unwatched +unweave +unwed +unwelcome +unwell +unwieldy +unwilling +unwind +unwired +unwitting +unwomanly +unworldly +unworn +unworried +unworthy +unwound +unwoven +unwrapped +unwritten +unzip +upbeat +upchuck +upcoming +upcountry +update +upfront +upgrade +upheaval +upheld +uphill +uphold +uplifted +uplifting +upload +upon +upper +upright +uprising +upriver +uproar +uproot +upscale +upside +upstage +upstairs +upstart +upstate +upstream +upstroke +upswing +uptake +uptight +uptown +upturned +upward +upwind +uranium +urban +urchin +urethane +urgency +urgent +urging +urologist +urology +usable +usage +useable +used +uselessly +user +usher +usual +utensil +utility +utilize +utmost +utopia +utter +vacancy +vacant +vacate +vacation +vagabond +vagrancy +vagrantly +vaguely +vagueness +valiant +valid +valium +valley +valuables +value +vanilla +vanish +vanity +vanquish +vantage +vaporizer +variable +variably +varied +variety +various +varmint +varnish +varsity +varying +vascular +vaseline +vastly +vastness +veal +vegan +veggie +vehicular +velcro +velocity +velvet +vendetta +vending +vendor +veneering +vengeful +venomous +ventricle +venture +venue +venus +verbalize +verbally +verbose +verdict +verify +verse +version +versus +vertebrae +vertical +vertigo +very +vessel +vest +veteran +veto +vexingly +viability +viable +vibes +vice +vicinity +victory +video +viewable +viewer +viewing +viewless +viewpoint +vigorous +village +villain +vindicate +vineyard +vintage +violate +violation +violator +violet +violin +viper +viral +virtual +virtuous +virus +visa +viscosity +viscous +viselike +visible +visibly +vision +visiting +visitor +visor +vista +vitality +vitalize +vitally +vitamins +vivacious +vividly +vividness +vixen +vocalist +vocalize +vocally +vocation +voice +voicing +void +volatile +volley +voltage +volumes +voter +voting +voucher +vowed +vowel +voyage +wackiness +wad +wafer +waffle +waged +wager +wages +waggle +wagon +wake +waking +walk +walmart +walnut +walrus +waltz +wand +wannabe +wanted +wanting +wasabi +washable +washbasin +washboard +washbowl +washcloth +washday +washed +washer +washhouse +washing +washout +washroom +washstand +washtub +wasp +wasting +watch +water +waviness +waving +wavy +whacking +whacky +wham +wharf +wheat +whenever +whiff +whimsical +whinny +whiny +whisking +whoever +whole +whomever +whoopee +whooping +whoops +why +wick +widely +widen +widget +widow +width +wieldable +wielder +wife +wifi +wikipedia +wildcard +wildcat +wilder +wildfire +wildfowl +wildland +wildlife +wildly +wildness +willed +willfully +willing +willow +willpower +wilt +wimp +wince +wincing +wind +wing +winking +winner +winnings +winter +wipe +wired +wireless +wiring +wiry +wisdom +wise +wish +wisplike +wispy +wistful +wizard +wobble +wobbling +wobbly +wok +wolf +wolverine +womanhood +womankind +womanless +womanlike +womanly +womb +woof +wooing +wool +woozy +word +work +worried +worrier +worrisome +worry +worsening +worshiper +worst +wound +woven +wow +wrangle +wrath +wreath +wreckage +wrecker +wrecking +wrench +wriggle +wriggly +wrinkle +wrinkly +wrist +writing +written +wrongdoer +wronged +wrongful +wrongly +wrongness +wrought +xbox +xerox +yahoo +yam +yanking +yapping +yard +yarn +yeah +yearbook +yearling +yearly +yearning +yeast +yelling +yelp +yen +yesterday +yiddish +yield +yin +yippee +yo-yo +yodel +yoga +yogurt +yonder +yoyo +yummy +zap +zealous +zebra +zen +zeppelin +zero +zestfully +zesty +zigzagged +zipfile +zipping +zippy +zips +zit +zodiac +zombie +zone +zoning +zookeeper +zoologist +zoology +zoom \ No newline at end of file diff --git a/src/Maui/Bitwarden/Core/Resources/public_suffix_list.dat b/src/Maui/Bitwarden/Core/Resources/public_suffix_list.dat new file mode 100644 index 000000000..b1eab04cf --- /dev/null +++ b/src/Maui/Bitwarden/Core/Resources/public_suffix_list.dat @@ -0,0 +1,12653 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Please pull this list from, and only from https://publicsuffix.org/list/public_suffix_list.dat, +// rather than any other VCS sites. Pulling from any other URL is not guaranteed to be supported. + +// Instructions on pulling and using this list can be found at https://publicsuffix.org/list/. + +// ===BEGIN ICANN DOMAINS=== + +// ac : https://en.wikipedia.org/wiki/.ac +ac +com.ac +edu.ac +gov.ac +net.ac +mil.ac +org.ac + +// ad : https://en.wikipedia.org/wiki/.ad +ad +nom.ad + +// ae : https://en.wikipedia.org/wiki/.ae +// see also: "Domain Name Eligibility Policy" at http://www.aeda.ae/eng/aepolicy.php +ae +co.ae +net.ae +org.ae +sch.ae +ac.ae +gov.ae +mil.ae + +// aero : see https://www.information.aero/index.php?id=66 +aero +accident-investigation.aero +accident-prevention.aero +aerobatic.aero +aeroclub.aero +aerodrome.aero +agents.aero +aircraft.aero +airline.aero +airport.aero +air-surveillance.aero +airtraffic.aero +air-traffic-control.aero +ambulance.aero +amusement.aero +association.aero +author.aero +ballooning.aero +broker.aero +caa.aero +cargo.aero +catering.aero +certification.aero +championship.aero +charter.aero +civilaviation.aero +club.aero +conference.aero +consultant.aero +consulting.aero +control.aero +council.aero +crew.aero +design.aero +dgca.aero +educator.aero +emergency.aero +engine.aero +engineer.aero +entertainment.aero +equipment.aero +exchange.aero +express.aero +federation.aero +flight.aero +freight.aero +fuel.aero +gliding.aero +government.aero +groundhandling.aero +group.aero +hanggliding.aero +homebuilt.aero +insurance.aero +journal.aero +journalist.aero +leasing.aero +logistics.aero +magazine.aero +maintenance.aero +media.aero +microlight.aero +modelling.aero +navigation.aero +parachuting.aero +paragliding.aero +passenger-association.aero +pilot.aero +press.aero +production.aero +recreation.aero +repbody.aero +res.aero +research.aero +rotorcraft.aero +safety.aero +scientist.aero +services.aero +show.aero +skydiving.aero +software.aero +student.aero +trader.aero +trading.aero +trainer.aero +union.aero +workinggroup.aero +works.aero + +// af : http://www.nic.af/help.jsp +af +gov.af +com.af +org.af +net.af +edu.af + +// ag : http://www.nic.ag/prices.htm +ag +com.ag +org.ag +net.ag +co.ag +nom.ag + +// ai : http://nic.com.ai/ +ai +off.ai +com.ai +net.ai +org.ai + +// al : http://www.ert.gov.al/ert_alb/faq_det.html?Id=31 +al +com.al +edu.al +gov.al +mil.al +net.al +org.al + +// am : https://en.wikipedia.org/wiki/.am +am + +// ao : https://en.wikipedia.org/wiki/.ao +// http://www.dns.ao/REGISTR.DOC +ao +ed.ao +gv.ao +og.ao +co.ao +pb.ao +it.ao + +// aq : https://en.wikipedia.org/wiki/.aq +aq + +// ar : https://nic.ar/nic-argentina/normativa-vigente +ar +com.ar +edu.ar +gob.ar +gov.ar +int.ar +mil.ar +musica.ar +net.ar +org.ar +tur.ar + +// arpa : https://en.wikipedia.org/wiki/.arpa +// Confirmed by registry 2008-06-18 +arpa +e164.arpa +in-addr.arpa +ip6.arpa +iris.arpa +uri.arpa +urn.arpa + +// as : https://en.wikipedia.org/wiki/.as +as +gov.as + +// asia : https://en.wikipedia.org/wiki/.asia +asia + +// at : https://en.wikipedia.org/wiki/.at +// Confirmed by registry 2008-06-17 +at +ac.at +co.at +gv.at +or.at + +// au : https://en.wikipedia.org/wiki/.au +// http://www.auda.org.au/ +au +// 2LDs +com.au +net.au +org.au +edu.au +gov.au +asn.au +id.au +// Historic 2LDs (closed to new registration, but sites still exist) +info.au +conf.au +oz.au +// CGDNs - http://www.cgdn.org.au/ +act.au +nsw.au +nt.au +qld.au +sa.au +tas.au +vic.au +wa.au +// 3LDs +act.edu.au +nsw.edu.au +nt.edu.au +qld.edu.au +sa.edu.au +tas.edu.au +vic.edu.au +wa.edu.au +// act.gov.au Bug 984824 - Removed at request of Greg Tankard +// nsw.gov.au Bug 547985 - Removed at request of +// nt.gov.au Bug 940478 - Removed at request of Greg Connors +qld.gov.au +sa.gov.au +tas.gov.au +vic.gov.au +wa.gov.au + +// aw : https://en.wikipedia.org/wiki/.aw +aw +com.aw + +// ax : https://en.wikipedia.org/wiki/.ax +ax + +// az : https://en.wikipedia.org/wiki/.az +az +com.az +net.az +int.az +gov.az +org.az +edu.az +info.az +pp.az +mil.az +name.az +pro.az +biz.az + +// ba : http://nic.ba/users_data/files/pravilnik_o_registraciji.pdf +ba +com.ba +edu.ba +gov.ba +mil.ba +net.ba +org.ba + +// bb : https://en.wikipedia.org/wiki/.bb +bb +biz.bb +co.bb +com.bb +edu.bb +gov.bb +info.bb +net.bb +org.bb +store.bb +tv.bb + +// bd : https://en.wikipedia.org/wiki/.bd +*.bd + +// be : https://en.wikipedia.org/wiki/.be +// Confirmed by registry 2008-06-08 +be +ac.be + +// bf : https://en.wikipedia.org/wiki/.bf +bf +gov.bf + +// bg : https://en.wikipedia.org/wiki/.bg +// https://www.register.bg/user/static/rules/en/index.html +bg +a.bg +b.bg +c.bg +d.bg +e.bg +f.bg +g.bg +h.bg +i.bg +j.bg +k.bg +l.bg +m.bg +n.bg +o.bg +p.bg +q.bg +r.bg +s.bg +t.bg +u.bg +v.bg +w.bg +x.bg +y.bg +z.bg +0.bg +1.bg +2.bg +3.bg +4.bg +5.bg +6.bg +7.bg +8.bg +9.bg + +// bh : https://en.wikipedia.org/wiki/.bh +bh +com.bh +edu.bh +net.bh +org.bh +gov.bh + +// bi : https://en.wikipedia.org/wiki/.bi +// http://whois.nic.bi/ +bi +co.bi +com.bi +edu.bi +or.bi +org.bi + +// biz : https://en.wikipedia.org/wiki/.biz +biz + +// bj : https://en.wikipedia.org/wiki/.bj +bj +asso.bj +barreau.bj +gouv.bj + +// bm : http://www.bermudanic.bm/dnr-text.txt +bm +com.bm +edu.bm +gov.bm +net.bm +org.bm + +// bn : https://en.wikipedia.org/wiki/.bn +*.bn + +// bo : https://nic.bo/delegacion2015.php#h-1.10 +bo +com.bo +edu.bo +gob.bo +int.bo +org.bo +net.bo +mil.bo +tv.bo +web.bo +// Social Domains +academia.bo +agro.bo +arte.bo +blog.bo +bolivia.bo +ciencia.bo +cooperativa.bo +democracia.bo +deporte.bo +ecologia.bo +economia.bo +empresa.bo +indigena.bo +industria.bo +info.bo +medicina.bo +movimiento.bo +musica.bo +natural.bo +nombre.bo +noticias.bo +patria.bo +politica.bo +profesional.bo +plurinacional.bo +pueblo.bo +revista.bo +salud.bo +tecnologia.bo +tksat.bo +transporte.bo +wiki.bo + +// br : http://registro.br/dominio/categoria.html +// Submitted by registry +br +9guacu.br +abc.br +adm.br +adv.br +agr.br +aju.br +am.br +anani.br +aparecida.br +arq.br +art.br +ato.br +b.br +barueri.br +belem.br +bhz.br +bio.br +blog.br +bmd.br +boavista.br +bsb.br +campinagrande.br +campinas.br +caxias.br +cim.br +cng.br +cnt.br +com.br +contagem.br +coop.br +cri.br +cuiaba.br +curitiba.br +def.br +ecn.br +eco.br +edu.br +emp.br +eng.br +esp.br +etc.br +eti.br +far.br +feira.br +flog.br +floripa.br +fm.br +fnd.br +fortal.br +fot.br +foz.br +fst.br +g12.br +ggf.br +goiania.br +gov.br +// gov.br 26 states + df https://en.wikipedia.org/wiki/States_of_Brazil +ac.gov.br +al.gov.br +am.gov.br +ap.gov.br +ba.gov.br +ce.gov.br +df.gov.br +es.gov.br +go.gov.br +ma.gov.br +mg.gov.br +ms.gov.br +mt.gov.br +pa.gov.br +pb.gov.br +pe.gov.br +pi.gov.br +pr.gov.br +rj.gov.br +rn.gov.br +ro.gov.br +rr.gov.br +rs.gov.br +sc.gov.br +se.gov.br +sp.gov.br +to.gov.br +gru.br +imb.br +ind.br +inf.br +jab.br +jampa.br +jdf.br +joinville.br +jor.br +jus.br +leg.br +lel.br +londrina.br +macapa.br +maceio.br +manaus.br +maringa.br +mat.br +med.br +mil.br +morena.br +mp.br +mus.br +natal.br +net.br +niteroi.br +*.nom.br +not.br +ntr.br +odo.br +org.br +osasco.br +palmas.br +poa.br +ppg.br +pro.br +psc.br +psi.br +pvh.br +qsl.br +radio.br +rec.br +recife.br +ribeirao.br +rio.br +riobranco.br +riopreto.br +salvador.br +sampa.br +santamaria.br +santoandre.br +saobernardo.br +saogonca.br +sjc.br +slg.br +slz.br +sorocaba.br +srv.br +taxi.br +teo.br +the.br +tmp.br +trd.br +tur.br +tv.br +udi.br +vet.br +vix.br +vlog.br +wiki.br +zlg.br + +// bs : http://www.nic.bs/rules.html +bs +com.bs +net.bs +org.bs +edu.bs +gov.bs + +// bt : https://en.wikipedia.org/wiki/.bt +bt +com.bt +edu.bt +gov.bt +net.bt +org.bt + +// bv : No registrations at this time. +// Submitted by registry +bv + +// bw : https://en.wikipedia.org/wiki/.bw +// http://www.gobin.info/domainname/bw.doc +// list of other 2nd level tlds ? +bw +co.bw +org.bw + +// by : https://en.wikipedia.org/wiki/.by +// http://tld.by/rules_2006_en.html +// list of other 2nd level tlds ? +by +gov.by +mil.by +// Official information does not indicate that com.by is a reserved +// second-level domain, but it's being used as one (see www.google.com.by and +// www.yahoo.com.by, for example), so we list it here for safety's sake. +com.by + +// http://hoster.by/ +of.by + +// bz : https://en.wikipedia.org/wiki/.bz +// http://www.belizenic.bz/ +bz +com.bz +net.bz +org.bz +edu.bz +gov.bz + +// ca : https://en.wikipedia.org/wiki/.ca +ca +// ca geographical names +ab.ca +bc.ca +mb.ca +nb.ca +nf.ca +nl.ca +ns.ca +nt.ca +nu.ca +on.ca +pe.ca +qc.ca +sk.ca +yk.ca +// gc.ca: https://en.wikipedia.org/wiki/.gc.ca +// see also: http://registry.gc.ca/en/SubdomainFAQ +gc.ca + +// cat : https://en.wikipedia.org/wiki/.cat +cat + +// cc : https://en.wikipedia.org/wiki/.cc +cc + +// cd : https://en.wikipedia.org/wiki/.cd +// see also: https://www.nic.cd/domain/insertDomain_2.jsp?act=1 +cd +gov.cd + +// cf : https://en.wikipedia.org/wiki/.cf +cf + +// cg : https://en.wikipedia.org/wiki/.cg +cg + +// ch : https://en.wikipedia.org/wiki/.ch +ch + +// ci : https://en.wikipedia.org/wiki/.ci +// http://www.nic.ci/index.php?page=charte +ci +org.ci +or.ci +com.ci +co.ci +edu.ci +ed.ci +ac.ci +net.ci +go.ci +asso.ci +aéroport.ci +int.ci +presse.ci +md.ci +gouv.ci + +// ck : https://en.wikipedia.org/wiki/.ck +*.ck +!www.ck + +// cl : https://en.wikipedia.org/wiki/.cl +cl +gov.cl +gob.cl +co.cl +mil.cl + +// cm : https://en.wikipedia.org/wiki/.cm plus bug 981927 +cm +co.cm +com.cm +gov.cm +net.cm + +// cn : https://en.wikipedia.org/wiki/.cn +// Submitted by registry +cn +ac.cn +com.cn +edu.cn +gov.cn +net.cn +org.cn +mil.cn +公司.cn +网络.cn +網絡.cn +// cn geographic names +ah.cn +bj.cn +cq.cn +fj.cn +gd.cn +gs.cn +gz.cn +gx.cn +ha.cn +hb.cn +he.cn +hi.cn +hl.cn +hn.cn +jl.cn +js.cn +jx.cn +ln.cn +nm.cn +nx.cn +qh.cn +sc.cn +sd.cn +sh.cn +sn.cn +sx.cn +tj.cn +xj.cn +xz.cn +yn.cn +zj.cn +hk.cn +mo.cn +tw.cn + +// co : https://en.wikipedia.org/wiki/.co +// Submitted by registry +co +arts.co +com.co +edu.co +firm.co +gov.co +info.co +int.co +mil.co +net.co +nom.co +org.co +rec.co +web.co + +// com : https://en.wikipedia.org/wiki/.com +com + +// coop : https://en.wikipedia.org/wiki/.coop +coop + +// cr : http://www.nic.cr/niccr_publico/showRegistroDominiosScreen.do +cr +ac.cr +co.cr +ed.cr +fi.cr +go.cr +or.cr +sa.cr + +// cu : https://en.wikipedia.org/wiki/.cu +cu +com.cu +edu.cu +org.cu +net.cu +gov.cu +inf.cu + +// cv : https://en.wikipedia.org/wiki/.cv +cv + +// cw : http://www.una.cw/cw_registry/ +// Confirmed by registry 2013-03-26 +cw +com.cw +edu.cw +net.cw +org.cw + +// cx : https://en.wikipedia.org/wiki/.cx +// list of other 2nd level tlds ? +cx +gov.cx + +// cy : http://www.nic.cy/ +// Submitted by registry Panayiotou Fotia +cy +ac.cy +biz.cy +com.cy +ekloges.cy +gov.cy +ltd.cy +name.cy +net.cy +org.cy +parliament.cy +press.cy +pro.cy +tm.cy + +// cz : https://en.wikipedia.org/wiki/.cz +cz + +// de : https://en.wikipedia.org/wiki/.de +// Confirmed by registry (with technical +// reservations) 2008-07-01 +de + +// dj : https://en.wikipedia.org/wiki/.dj +dj + +// dk : https://en.wikipedia.org/wiki/.dk +// Confirmed by registry 2008-06-17 +dk + +// dm : https://en.wikipedia.org/wiki/.dm +dm +com.dm +net.dm +org.dm +edu.dm +gov.dm + +// do : https://en.wikipedia.org/wiki/.do +do +art.do +com.do +edu.do +gob.do +gov.do +mil.do +net.do +org.do +sld.do +web.do + +// dz : https://en.wikipedia.org/wiki/.dz +dz +com.dz +org.dz +net.dz +gov.dz +edu.dz +asso.dz +pol.dz +art.dz + +// ec : http://www.nic.ec/reg/paso1.asp +// Submitted by registry +ec +com.ec +info.ec +net.ec +fin.ec +k12.ec +med.ec +pro.ec +org.ec +edu.ec +gov.ec +gob.ec +mil.ec + +// edu : https://en.wikipedia.org/wiki/.edu +edu + +// ee : http://www.eenet.ee/EENet/dom_reeglid.html#lisa_B +ee +edu.ee +gov.ee +riik.ee +lib.ee +med.ee +com.ee +pri.ee +aip.ee +org.ee +fie.ee + +// eg : https://en.wikipedia.org/wiki/.eg +eg +com.eg +edu.eg +eun.eg +gov.eg +mil.eg +name.eg +net.eg +org.eg +sci.eg + +// er : https://en.wikipedia.org/wiki/.er +*.er + +// es : https://www.nic.es/site_ingles/ingles/dominios/index.html +es +com.es +nom.es +org.es +gob.es +edu.es + +// et : https://en.wikipedia.org/wiki/.et +et +com.et +gov.et +org.et +edu.et +biz.et +name.et +info.et +net.et + +// eu : https://en.wikipedia.org/wiki/.eu +eu + +// fi : https://en.wikipedia.org/wiki/.fi +fi +// aland.fi : https://en.wikipedia.org/wiki/.ax +// This domain is being phased out in favor of .ax. As there are still many +// domains under aland.fi, we still keep it on the list until aland.fi is +// completely removed. +// TODO: Check for updates (expected to be phased out around Q1/2009) +aland.fi + +// fj : https://en.wikipedia.org/wiki/.fj +*.fj + +// fk : https://en.wikipedia.org/wiki/.fk +*.fk + +// fm : https://en.wikipedia.org/wiki/.fm +fm + +// fo : https://en.wikipedia.org/wiki/.fo +fo + +// fr : http://www.afnic.fr/ +// domaines descriptifs : http://www.afnic.fr/obtenir/chartes/nommage-fr/annexe-descriptifs +fr +com.fr +asso.fr +nom.fr +prd.fr +presse.fr +tm.fr +// domaines sectoriels : http://www.afnic.fr/obtenir/chartes/nommage-fr/annexe-sectoriels +aeroport.fr +assedic.fr +avocat.fr +avoues.fr +cci.fr +chambagri.fr +chirurgiens-dentistes.fr +experts-comptables.fr +geometre-expert.fr +gouv.fr +greta.fr +huissier-justice.fr +medecin.fr +notaires.fr +pharmacien.fr +port.fr +veterinaire.fr + +// ga : https://en.wikipedia.org/wiki/.ga +ga + +// gb : This registry is effectively dormant +// Submitted by registry +gb + +// gd : https://en.wikipedia.org/wiki/.gd +gd + +// ge : http://www.nic.net.ge/policy_en.pdf +ge +com.ge +edu.ge +gov.ge +org.ge +mil.ge +net.ge +pvt.ge + +// gf : https://en.wikipedia.org/wiki/.gf +gf + +// gg : http://www.channelisles.net/register-domains/ +// Confirmed by registry 2013-11-28 +gg +co.gg +net.gg +org.gg + +// gh : https://en.wikipedia.org/wiki/.gh +// see also: http://www.nic.gh/reg_now.php +// Although domains directly at second level are not possible at the moment, +// they have been possible for some time and may come back. +gh +com.gh +edu.gh +gov.gh +org.gh +mil.gh + +// gi : http://www.nic.gi/rules.html +gi +com.gi +ltd.gi +gov.gi +mod.gi +edu.gi +org.gi + +// gl : https://en.wikipedia.org/wiki/.gl +// http://nic.gl +gl +co.gl +com.gl +edu.gl +net.gl +org.gl + +// gm : http://www.nic.gm/htmlpages%5Cgm-policy.htm +gm + +// gn : http://psg.com/dns/gn/gn.txt +// Submitted by registry +gn +ac.gn +com.gn +edu.gn +gov.gn +org.gn +net.gn + +// gov : https://en.wikipedia.org/wiki/.gov +gov + +// gp : http://www.nic.gp/index.php?lang=en +gp +com.gp +net.gp +mobi.gp +edu.gp +org.gp +asso.gp + +// gq : https://en.wikipedia.org/wiki/.gq +gq + +// gr : https://grweb.ics.forth.gr/english/1617-B-2005.html +// Submitted by registry +gr +com.gr +edu.gr +net.gr +org.gr +gov.gr + +// gs : https://en.wikipedia.org/wiki/.gs +gs + +// gt : http://www.gt/politicas_de_registro.html +gt +com.gt +edu.gt +gob.gt +ind.gt +mil.gt +net.gt +org.gt + +// gu : http://gadao.gov.gu/register.html +// University of Guam : https://www.uog.edu +// Submitted by uognoc@triton.uog.edu +gu +com.gu +edu.gu +gov.gu +guam.gu +info.gu +net.gu +org.gu +web.gu + +// gw : https://en.wikipedia.org/wiki/.gw +gw + +// gy : https://en.wikipedia.org/wiki/.gy +// http://registry.gy/ +gy +co.gy +com.gy +edu.gy +gov.gy +net.gy +org.gy + +// hk : https://www.hkirc.hk +// Submitted by registry +hk +com.hk +edu.hk +gov.hk +idv.hk +net.hk +org.hk +公司.hk +教育.hk +敎育.hk +政府.hk +個人.hk +个人.hk +箇人.hk +網络.hk +网络.hk +组織.hk +網絡.hk +网絡.hk +组织.hk +組織.hk +組织.hk + +// hm : https://en.wikipedia.org/wiki/.hm +hm + +// hn : http://www.nic.hn/politicas/ps02,,05.html +hn +com.hn +edu.hn +org.hn +net.hn +mil.hn +gob.hn + +// hr : http://www.dns.hr/documents/pdf/HRTLD-regulations.pdf +hr +iz.hr +from.hr +name.hr +com.hr + +// ht : http://www.nic.ht/info/charte.cfm +ht +com.ht +shop.ht +firm.ht +info.ht +adult.ht +net.ht +pro.ht +org.ht +med.ht +art.ht +coop.ht +pol.ht +asso.ht +edu.ht +rel.ht +gouv.ht +perso.ht + +// hu : http://www.domain.hu/domain/English/sld.html +// Confirmed by registry 2008-06-12 +hu +co.hu +info.hu +org.hu +priv.hu +sport.hu +tm.hu +2000.hu +agrar.hu +bolt.hu +casino.hu +city.hu +erotica.hu +erotika.hu +film.hu +forum.hu +games.hu +hotel.hu +ingatlan.hu +jogasz.hu +konyvelo.hu +lakas.hu +media.hu +news.hu +reklam.hu +sex.hu +shop.hu +suli.hu +szex.hu +tozsde.hu +utazas.hu +video.hu + +// id : https://register.pandi.or.id/ +id +ac.id +biz.id +co.id +desa.id +go.id +mil.id +my.id +net.id +or.id +sch.id +web.id + +// ie : https://en.wikipedia.org/wiki/.ie +ie +gov.ie + +// il : http://www.isoc.org.il/domains/ +il +ac.il +co.il +gov.il +idf.il +k12.il +muni.il +net.il +org.il + +// im : https://www.nic.im/ +// Submitted by registry +im +ac.im +co.im +com.im +ltd.co.im +net.im +org.im +plc.co.im +tt.im +tv.im + +// in : https://en.wikipedia.org/wiki/.in +// see also: https://registry.in/Policies +// Please note, that nic.in is not an official eTLD, but used by most +// government institutions. +in +co.in +firm.in +net.in +org.in +gen.in +ind.in +nic.in +ac.in +edu.in +res.in +gov.in +mil.in + +// info : https://en.wikipedia.org/wiki/.info +info + +// int : https://en.wikipedia.org/wiki/.int +// Confirmed by registry 2008-06-18 +int +eu.int + +// io : http://www.nic.io/rules.html +// list of other 2nd level tlds ? +io +com.io + +// iq : http://www.cmc.iq/english/iq/iqregister1.htm +iq +gov.iq +edu.iq +mil.iq +com.iq +org.iq +net.iq + +// ir : http://www.nic.ir/Terms_and_Conditions_ir,_Appendix_1_Domain_Rules +// Also see http://www.nic.ir/Internationalized_Domain_Names +// Two .ir entries added at request of , 2010-04-16 +ir +ac.ir +co.ir +gov.ir +id.ir +net.ir +org.ir +sch.ir +// xn--mgba3a4f16a.ir (.ir, Persian YEH) +ایران.ir +// xn--mgba3a4fra.ir (.ir, Arabic YEH) +ايران.ir + +// is : http://www.isnic.is/domain/rules.php +// Confirmed by registry 2008-12-06 +is +net.is +com.is +edu.is +gov.is +org.is +int.is + +// it : https://en.wikipedia.org/wiki/.it +it +gov.it +edu.it +// Reserved geo-names (regions and provinces): +// http://www.nic.it/sites/default/files/docs/Regulation_assignation_v7.1.pdf +// Regions +abr.it +abruzzo.it +aosta-valley.it +aostavalley.it +bas.it +basilicata.it +cal.it +calabria.it +cam.it +campania.it +emilia-romagna.it +emiliaromagna.it +emr.it +friuli-v-giulia.it +friuli-ve-giulia.it +friuli-vegiulia.it +friuli-venezia-giulia.it +friuli-veneziagiulia.it +friuli-vgiulia.it +friuliv-giulia.it +friulive-giulia.it +friulivegiulia.it +friulivenezia-giulia.it +friuliveneziagiulia.it +friulivgiulia.it +fvg.it +laz.it +lazio.it +lig.it +liguria.it +lom.it +lombardia.it +lombardy.it +lucania.it +mar.it +marche.it +mol.it +molise.it +piedmont.it +piemonte.it +pmn.it +pug.it +puglia.it +sar.it +sardegna.it +sardinia.it +sic.it +sicilia.it +sicily.it +taa.it +tos.it +toscana.it +trentin-sud-tirol.it +trentin-süd-tirol.it +trentin-sudtirol.it +trentin-südtirol.it +trentin-sued-tirol.it +trentin-suedtirol.it +trentino-a-adige.it +trentino-aadige.it +trentino-alto-adige.it +trentino-altoadige.it +trentino-s-tirol.it +trentino-stirol.it +trentino-sud-tirol.it +trentino-süd-tirol.it +trentino-sudtirol.it +trentino-südtirol.it +trentino-sued-tirol.it +trentino-suedtirol.it +trentino.it +trentinoa-adige.it +trentinoaadige.it +trentinoalto-adige.it +trentinoaltoadige.it +trentinos-tirol.it +trentinostirol.it +trentinosud-tirol.it +trentinosüd-tirol.it +trentinosudtirol.it +trentinosüdtirol.it +trentinosued-tirol.it +trentinosuedtirol.it +trentinsud-tirol.it +trentinsüd-tirol.it +trentinsudtirol.it +trentinsüdtirol.it +trentinsued-tirol.it +trentinsuedtirol.it +tuscany.it +umb.it +umbria.it +val-d-aosta.it +val-daosta.it +vald-aosta.it +valdaosta.it +valle-aosta.it +valle-d-aosta.it +valle-daosta.it +valleaosta.it +valled-aosta.it +valledaosta.it +vallee-aoste.it +vallée-aoste.it +vallee-d-aoste.it +vallée-d-aoste.it +valleeaoste.it +valléeaoste.it +valleedaoste.it +valléedaoste.it +vao.it +vda.it +ven.it +veneto.it +// Provinces +ag.it +agrigento.it +al.it +alessandria.it +alto-adige.it +altoadige.it +an.it +ancona.it +andria-barletta-trani.it +andria-trani-barletta.it +andriabarlettatrani.it +andriatranibarletta.it +ao.it +aosta.it +aoste.it +ap.it +aq.it +aquila.it +ar.it +arezzo.it +ascoli-piceno.it +ascolipiceno.it +asti.it +at.it +av.it +avellino.it +ba.it +balsan-sudtirol.it +balsan-südtirol.it +balsan-suedtirol.it +balsan.it +bari.it +barletta-trani-andria.it +barlettatraniandria.it +belluno.it +benevento.it +bergamo.it +bg.it +bi.it +biella.it +bl.it +bn.it +bo.it +bologna.it +bolzano-altoadige.it +bolzano.it +bozen-sudtirol.it +bozen-südtirol.it +bozen-suedtirol.it +bozen.it +br.it +brescia.it +brindisi.it +bs.it +bt.it +bulsan-sudtirol.it +bulsan-südtirol.it +bulsan-suedtirol.it +bulsan.it +bz.it +ca.it +cagliari.it +caltanissetta.it +campidano-medio.it +campidanomedio.it +campobasso.it +carbonia-iglesias.it +carboniaiglesias.it +carrara-massa.it +carraramassa.it +caserta.it +catania.it +catanzaro.it +cb.it +ce.it +cesena-forli.it +cesena-forlì.it +cesenaforli.it +cesenaforlì.it +ch.it +chieti.it +ci.it +cl.it +cn.it +co.it +como.it +cosenza.it +cr.it +cremona.it +crotone.it +cs.it +ct.it +cuneo.it +cz.it +dell-ogliastra.it +dellogliastra.it +en.it +enna.it +fc.it +fe.it +fermo.it +ferrara.it +fg.it +fi.it +firenze.it +florence.it +fm.it +foggia.it +forli-cesena.it +forlì-cesena.it +forlicesena.it +forlìcesena.it +fr.it +frosinone.it +ge.it +genoa.it +genova.it +go.it +gorizia.it +gr.it +grosseto.it +iglesias-carbonia.it +iglesiascarbonia.it +im.it +imperia.it +is.it +isernia.it +kr.it +la-spezia.it +laquila.it +laspezia.it +latina.it +lc.it +le.it +lecce.it +lecco.it +li.it +livorno.it +lo.it +lodi.it +lt.it +lu.it +lucca.it +macerata.it +mantova.it +massa-carrara.it +massacarrara.it +matera.it +mb.it +mc.it +me.it +medio-campidano.it +mediocampidano.it +messina.it +mi.it +milan.it +milano.it +mn.it +mo.it +modena.it +monza-brianza.it +monza-e-della-brianza.it +monza.it +monzabrianza.it +monzaebrianza.it +monzaedellabrianza.it +ms.it +mt.it +na.it +naples.it +napoli.it +no.it +novara.it +nu.it +nuoro.it +og.it +ogliastra.it +olbia-tempio.it +olbiatempio.it +or.it +oristano.it +ot.it +pa.it +padova.it +padua.it +palermo.it +parma.it +pavia.it +pc.it +pd.it +pe.it +perugia.it +pesaro-urbino.it +pesarourbino.it +pescara.it +pg.it +pi.it +piacenza.it +pisa.it +pistoia.it +pn.it +po.it +pordenone.it +potenza.it +pr.it +prato.it +pt.it +pu.it +pv.it +pz.it +ra.it +ragusa.it +ravenna.it +rc.it +re.it +reggio-calabria.it +reggio-emilia.it +reggiocalabria.it +reggioemilia.it +rg.it +ri.it +rieti.it +rimini.it +rm.it +rn.it +ro.it +roma.it +rome.it +rovigo.it +sa.it +salerno.it +sassari.it +savona.it +si.it +siena.it +siracusa.it +so.it +sondrio.it +sp.it +sr.it +ss.it +suedtirol.it +südtirol.it +sv.it +ta.it +taranto.it +te.it +tempio-olbia.it +tempioolbia.it +teramo.it +terni.it +tn.it +to.it +torino.it +tp.it +tr.it +trani-andria-barletta.it +trani-barletta-andria.it +traniandriabarletta.it +tranibarlettaandria.it +trapani.it +trento.it +treviso.it +trieste.it +ts.it +turin.it +tv.it +ud.it +udine.it +urbino-pesaro.it +urbinopesaro.it +va.it +varese.it +vb.it +vc.it +ve.it +venezia.it +venice.it +verbania.it +vercelli.it +verona.it +vi.it +vibo-valentia.it +vibovalentia.it +vicenza.it +viterbo.it +vr.it +vs.it +vt.it +vv.it + +// je : http://www.channelisles.net/register-domains/ +// Confirmed by registry 2013-11-28 +je +co.je +net.je +org.je + +// jm : http://www.com.jm/register.html +*.jm + +// jo : http://www.dns.jo/Registration_policy.aspx +jo +com.jo +org.jo +net.jo +edu.jo +sch.jo +gov.jo +mil.jo +name.jo + +// jobs : https://en.wikipedia.org/wiki/.jobs +jobs + +// jp : https://en.wikipedia.org/wiki/.jp +// http://jprs.co.jp/en/jpdomain.html +// Submitted by registry +jp +// jp organizational type names +ac.jp +ad.jp +co.jp +ed.jp +go.jp +gr.jp +lg.jp +ne.jp +or.jp +// jp prefecture type names +aichi.jp +akita.jp +aomori.jp +chiba.jp +ehime.jp +fukui.jp +fukuoka.jp +fukushima.jp +gifu.jp +gunma.jp +hiroshima.jp +hokkaido.jp +hyogo.jp +ibaraki.jp +ishikawa.jp +iwate.jp +kagawa.jp +kagoshima.jp +kanagawa.jp +kochi.jp +kumamoto.jp +kyoto.jp +mie.jp +miyagi.jp +miyazaki.jp +nagano.jp +nagasaki.jp +nara.jp +niigata.jp +oita.jp +okayama.jp +okinawa.jp +osaka.jp +saga.jp +saitama.jp +shiga.jp +shimane.jp +shizuoka.jp +tochigi.jp +tokushima.jp +tokyo.jp +tottori.jp +toyama.jp +wakayama.jp +yamagata.jp +yamaguchi.jp +yamanashi.jp +栃木.jp +愛知.jp +愛媛.jp +兵庫.jp +熊本.jp +茨城.jp +北海道.jp +千葉.jp +和歌山.jp +長崎.jp +長野.jp +新潟.jp +青森.jp +静岡.jp +東京.jp +石川.jp +埼玉.jp +三重.jp +京都.jp +佐賀.jp +大分.jp +大阪.jp +奈良.jp +宮城.jp +宮崎.jp +富山.jp +山口.jp +山形.jp +山梨.jp +岩手.jp +岐阜.jp +岡山.jp +島根.jp +広島.jp +徳島.jp +沖縄.jp +滋賀.jp +神奈川.jp +福井.jp +福岡.jp +福島.jp +秋田.jp +群馬.jp +香川.jp +高知.jp +鳥取.jp +鹿児島.jp +// jp geographic type names +// http://jprs.jp/doc/rule/saisoku-1.html +*.kawasaki.jp +*.kitakyushu.jp +*.kobe.jp +*.nagoya.jp +*.sapporo.jp +*.sendai.jp +*.yokohama.jp +!city.kawasaki.jp +!city.kitakyushu.jp +!city.kobe.jp +!city.nagoya.jp +!city.sapporo.jp +!city.sendai.jp +!city.yokohama.jp +// 4th level registration +aisai.aichi.jp +ama.aichi.jp +anjo.aichi.jp +asuke.aichi.jp +chiryu.aichi.jp +chita.aichi.jp +fuso.aichi.jp +gamagori.aichi.jp +handa.aichi.jp +hazu.aichi.jp +hekinan.aichi.jp +higashiura.aichi.jp +ichinomiya.aichi.jp +inazawa.aichi.jp +inuyama.aichi.jp +isshiki.aichi.jp +iwakura.aichi.jp +kanie.aichi.jp +kariya.aichi.jp +kasugai.aichi.jp +kira.aichi.jp +kiyosu.aichi.jp +komaki.aichi.jp +konan.aichi.jp +kota.aichi.jp +mihama.aichi.jp +miyoshi.aichi.jp +nishio.aichi.jp +nisshin.aichi.jp +obu.aichi.jp +oguchi.aichi.jp +oharu.aichi.jp +okazaki.aichi.jp +owariasahi.aichi.jp +seto.aichi.jp +shikatsu.aichi.jp +shinshiro.aichi.jp +shitara.aichi.jp +tahara.aichi.jp +takahama.aichi.jp +tobishima.aichi.jp +toei.aichi.jp +togo.aichi.jp +tokai.aichi.jp +tokoname.aichi.jp +toyoake.aichi.jp +toyohashi.aichi.jp +toyokawa.aichi.jp +toyone.aichi.jp +toyota.aichi.jp +tsushima.aichi.jp +yatomi.aichi.jp +akita.akita.jp +daisen.akita.jp +fujisato.akita.jp +gojome.akita.jp +hachirogata.akita.jp +happou.akita.jp +higashinaruse.akita.jp +honjo.akita.jp +honjyo.akita.jp +ikawa.akita.jp +kamikoani.akita.jp +kamioka.akita.jp +katagami.akita.jp +kazuno.akita.jp +kitaakita.akita.jp +kosaka.akita.jp +kyowa.akita.jp +misato.akita.jp +mitane.akita.jp +moriyoshi.akita.jp +nikaho.akita.jp +noshiro.akita.jp +odate.akita.jp +oga.akita.jp +ogata.akita.jp +semboku.akita.jp +yokote.akita.jp +yurihonjo.akita.jp +aomori.aomori.jp +gonohe.aomori.jp +hachinohe.aomori.jp +hashikami.aomori.jp +hiranai.aomori.jp +hirosaki.aomori.jp +itayanagi.aomori.jp +kuroishi.aomori.jp +misawa.aomori.jp +mutsu.aomori.jp +nakadomari.aomori.jp +noheji.aomori.jp +oirase.aomori.jp +owani.aomori.jp +rokunohe.aomori.jp +sannohe.aomori.jp +shichinohe.aomori.jp +shingo.aomori.jp +takko.aomori.jp +towada.aomori.jp +tsugaru.aomori.jp +tsuruta.aomori.jp +abiko.chiba.jp +asahi.chiba.jp +chonan.chiba.jp +chosei.chiba.jp +choshi.chiba.jp +chuo.chiba.jp +funabashi.chiba.jp +futtsu.chiba.jp +hanamigawa.chiba.jp +ichihara.chiba.jp +ichikawa.chiba.jp +ichinomiya.chiba.jp +inzai.chiba.jp +isumi.chiba.jp +kamagaya.chiba.jp +kamogawa.chiba.jp +kashiwa.chiba.jp +katori.chiba.jp +katsuura.chiba.jp +kimitsu.chiba.jp +kisarazu.chiba.jp +kozaki.chiba.jp +kujukuri.chiba.jp +kyonan.chiba.jp +matsudo.chiba.jp +midori.chiba.jp +mihama.chiba.jp +minamiboso.chiba.jp +mobara.chiba.jp +mutsuzawa.chiba.jp +nagara.chiba.jp +nagareyama.chiba.jp +narashino.chiba.jp +narita.chiba.jp +noda.chiba.jp +oamishirasato.chiba.jp +omigawa.chiba.jp +onjuku.chiba.jp +otaki.chiba.jp +sakae.chiba.jp +sakura.chiba.jp +shimofusa.chiba.jp +shirako.chiba.jp +shiroi.chiba.jp +shisui.chiba.jp +sodegaura.chiba.jp +sosa.chiba.jp +tako.chiba.jp +tateyama.chiba.jp +togane.chiba.jp +tohnosho.chiba.jp +tomisato.chiba.jp +urayasu.chiba.jp +yachimata.chiba.jp +yachiyo.chiba.jp +yokaichiba.chiba.jp +yokoshibahikari.chiba.jp +yotsukaido.chiba.jp +ainan.ehime.jp +honai.ehime.jp +ikata.ehime.jp +imabari.ehime.jp +iyo.ehime.jp +kamijima.ehime.jp +kihoku.ehime.jp +kumakogen.ehime.jp +masaki.ehime.jp +matsuno.ehime.jp +matsuyama.ehime.jp +namikata.ehime.jp +niihama.ehime.jp +ozu.ehime.jp +saijo.ehime.jp +seiyo.ehime.jp +shikokuchuo.ehime.jp +tobe.ehime.jp +toon.ehime.jp +uchiko.ehime.jp +uwajima.ehime.jp +yawatahama.ehime.jp +echizen.fukui.jp +eiheiji.fukui.jp +fukui.fukui.jp +ikeda.fukui.jp +katsuyama.fukui.jp +mihama.fukui.jp +minamiechizen.fukui.jp +obama.fukui.jp +ohi.fukui.jp +ono.fukui.jp +sabae.fukui.jp +sakai.fukui.jp +takahama.fukui.jp +tsuruga.fukui.jp +wakasa.fukui.jp +ashiya.fukuoka.jp +buzen.fukuoka.jp +chikugo.fukuoka.jp +chikuho.fukuoka.jp +chikujo.fukuoka.jp +chikushino.fukuoka.jp +chikuzen.fukuoka.jp +chuo.fukuoka.jp +dazaifu.fukuoka.jp +fukuchi.fukuoka.jp +hakata.fukuoka.jp +higashi.fukuoka.jp +hirokawa.fukuoka.jp +hisayama.fukuoka.jp +iizuka.fukuoka.jp +inatsuki.fukuoka.jp +kaho.fukuoka.jp +kasuga.fukuoka.jp +kasuya.fukuoka.jp +kawara.fukuoka.jp +keisen.fukuoka.jp +koga.fukuoka.jp +kurate.fukuoka.jp +kurogi.fukuoka.jp +kurume.fukuoka.jp +minami.fukuoka.jp +miyako.fukuoka.jp +miyama.fukuoka.jp +miyawaka.fukuoka.jp +mizumaki.fukuoka.jp +munakata.fukuoka.jp +nakagawa.fukuoka.jp +nakama.fukuoka.jp +nishi.fukuoka.jp +nogata.fukuoka.jp +ogori.fukuoka.jp +okagaki.fukuoka.jp +okawa.fukuoka.jp +oki.fukuoka.jp +omuta.fukuoka.jp +onga.fukuoka.jp +onojo.fukuoka.jp +oto.fukuoka.jp +saigawa.fukuoka.jp +sasaguri.fukuoka.jp +shingu.fukuoka.jp +shinyoshitomi.fukuoka.jp +shonai.fukuoka.jp +soeda.fukuoka.jp +sue.fukuoka.jp +tachiarai.fukuoka.jp +tagawa.fukuoka.jp +takata.fukuoka.jp +toho.fukuoka.jp +toyotsu.fukuoka.jp +tsuiki.fukuoka.jp +ukiha.fukuoka.jp +umi.fukuoka.jp +usui.fukuoka.jp +yamada.fukuoka.jp +yame.fukuoka.jp +yanagawa.fukuoka.jp +yukuhashi.fukuoka.jp +aizubange.fukushima.jp +aizumisato.fukushima.jp +aizuwakamatsu.fukushima.jp +asakawa.fukushima.jp +bandai.fukushima.jp +date.fukushima.jp +fukushima.fukushima.jp +furudono.fukushima.jp +futaba.fukushima.jp +hanawa.fukushima.jp +higashi.fukushima.jp +hirata.fukushima.jp +hirono.fukushima.jp +iitate.fukushima.jp +inawashiro.fukushima.jp +ishikawa.fukushima.jp +iwaki.fukushima.jp +izumizaki.fukushima.jp +kagamiishi.fukushima.jp +kaneyama.fukushima.jp +kawamata.fukushima.jp +kitakata.fukushima.jp +kitashiobara.fukushima.jp +koori.fukushima.jp +koriyama.fukushima.jp +kunimi.fukushima.jp +miharu.fukushima.jp +mishima.fukushima.jp +namie.fukushima.jp +nango.fukushima.jp +nishiaizu.fukushima.jp +nishigo.fukushima.jp +okuma.fukushima.jp +omotego.fukushima.jp +ono.fukushima.jp +otama.fukushima.jp +samegawa.fukushima.jp +shimogo.fukushima.jp +shirakawa.fukushima.jp +showa.fukushima.jp +soma.fukushima.jp +sukagawa.fukushima.jp +taishin.fukushima.jp +tamakawa.fukushima.jp +tanagura.fukushima.jp +tenei.fukushima.jp +yabuki.fukushima.jp +yamato.fukushima.jp +yamatsuri.fukushima.jp +yanaizu.fukushima.jp +yugawa.fukushima.jp +anpachi.gifu.jp +ena.gifu.jp +gifu.gifu.jp +ginan.gifu.jp +godo.gifu.jp +gujo.gifu.jp +hashima.gifu.jp +hichiso.gifu.jp +hida.gifu.jp +higashishirakawa.gifu.jp +ibigawa.gifu.jp +ikeda.gifu.jp +kakamigahara.gifu.jp +kani.gifu.jp +kasahara.gifu.jp +kasamatsu.gifu.jp +kawaue.gifu.jp +kitagata.gifu.jp +mino.gifu.jp +minokamo.gifu.jp +mitake.gifu.jp +mizunami.gifu.jp +motosu.gifu.jp +nakatsugawa.gifu.jp +ogaki.gifu.jp +sakahogi.gifu.jp +seki.gifu.jp +sekigahara.gifu.jp +shirakawa.gifu.jp +tajimi.gifu.jp +takayama.gifu.jp +tarui.gifu.jp +toki.gifu.jp +tomika.gifu.jp +wanouchi.gifu.jp +yamagata.gifu.jp +yaotsu.gifu.jp +yoro.gifu.jp +annaka.gunma.jp +chiyoda.gunma.jp +fujioka.gunma.jp +higashiagatsuma.gunma.jp +isesaki.gunma.jp +itakura.gunma.jp +kanna.gunma.jp +kanra.gunma.jp +katashina.gunma.jp +kawaba.gunma.jp +kiryu.gunma.jp +kusatsu.gunma.jp +maebashi.gunma.jp +meiwa.gunma.jp +midori.gunma.jp +minakami.gunma.jp +naganohara.gunma.jp +nakanojo.gunma.jp +nanmoku.gunma.jp +numata.gunma.jp +oizumi.gunma.jp +ora.gunma.jp +ota.gunma.jp +shibukawa.gunma.jp +shimonita.gunma.jp +shinto.gunma.jp +showa.gunma.jp +takasaki.gunma.jp +takayama.gunma.jp +tamamura.gunma.jp +tatebayashi.gunma.jp +tomioka.gunma.jp +tsukiyono.gunma.jp +tsumagoi.gunma.jp +ueno.gunma.jp +yoshioka.gunma.jp +asaminami.hiroshima.jp +daiwa.hiroshima.jp +etajima.hiroshima.jp +fuchu.hiroshima.jp +fukuyama.hiroshima.jp +hatsukaichi.hiroshima.jp +higashihiroshima.hiroshima.jp +hongo.hiroshima.jp +jinsekikogen.hiroshima.jp +kaita.hiroshima.jp +kui.hiroshima.jp +kumano.hiroshima.jp +kure.hiroshima.jp +mihara.hiroshima.jp +miyoshi.hiroshima.jp +naka.hiroshima.jp +onomichi.hiroshima.jp +osakikamijima.hiroshima.jp +otake.hiroshima.jp +saka.hiroshima.jp +sera.hiroshima.jp +seranishi.hiroshima.jp +shinichi.hiroshima.jp +shobara.hiroshima.jp +takehara.hiroshima.jp +abashiri.hokkaido.jp +abira.hokkaido.jp +aibetsu.hokkaido.jp +akabira.hokkaido.jp +akkeshi.hokkaido.jp +asahikawa.hokkaido.jp +ashibetsu.hokkaido.jp +ashoro.hokkaido.jp +assabu.hokkaido.jp +atsuma.hokkaido.jp +bibai.hokkaido.jp +biei.hokkaido.jp +bifuka.hokkaido.jp +bihoro.hokkaido.jp +biratori.hokkaido.jp +chippubetsu.hokkaido.jp +chitose.hokkaido.jp +date.hokkaido.jp +ebetsu.hokkaido.jp +embetsu.hokkaido.jp +eniwa.hokkaido.jp +erimo.hokkaido.jp +esan.hokkaido.jp +esashi.hokkaido.jp +fukagawa.hokkaido.jp +fukushima.hokkaido.jp +furano.hokkaido.jp +furubira.hokkaido.jp +haboro.hokkaido.jp +hakodate.hokkaido.jp +hamatonbetsu.hokkaido.jp +hidaka.hokkaido.jp +higashikagura.hokkaido.jp +higashikawa.hokkaido.jp +hiroo.hokkaido.jp +hokuryu.hokkaido.jp +hokuto.hokkaido.jp +honbetsu.hokkaido.jp +horokanai.hokkaido.jp +horonobe.hokkaido.jp +ikeda.hokkaido.jp +imakane.hokkaido.jp +ishikari.hokkaido.jp +iwamizawa.hokkaido.jp +iwanai.hokkaido.jp +kamifurano.hokkaido.jp +kamikawa.hokkaido.jp +kamishihoro.hokkaido.jp +kamisunagawa.hokkaido.jp +kamoenai.hokkaido.jp +kayabe.hokkaido.jp +kembuchi.hokkaido.jp +kikonai.hokkaido.jp +kimobetsu.hokkaido.jp +kitahiroshima.hokkaido.jp +kitami.hokkaido.jp +kiyosato.hokkaido.jp +koshimizu.hokkaido.jp +kunneppu.hokkaido.jp +kuriyama.hokkaido.jp +kuromatsunai.hokkaido.jp +kushiro.hokkaido.jp +kutchan.hokkaido.jp +kyowa.hokkaido.jp +mashike.hokkaido.jp +matsumae.hokkaido.jp +mikasa.hokkaido.jp +minamifurano.hokkaido.jp +mombetsu.hokkaido.jp +moseushi.hokkaido.jp +mukawa.hokkaido.jp +muroran.hokkaido.jp +naie.hokkaido.jp +nakagawa.hokkaido.jp +nakasatsunai.hokkaido.jp +nakatombetsu.hokkaido.jp +nanae.hokkaido.jp +nanporo.hokkaido.jp +nayoro.hokkaido.jp +nemuro.hokkaido.jp +niikappu.hokkaido.jp +niki.hokkaido.jp +nishiokoppe.hokkaido.jp +noboribetsu.hokkaido.jp +numata.hokkaido.jp +obihiro.hokkaido.jp +obira.hokkaido.jp +oketo.hokkaido.jp +okoppe.hokkaido.jp +otaru.hokkaido.jp +otobe.hokkaido.jp +otofuke.hokkaido.jp +otoineppu.hokkaido.jp +oumu.hokkaido.jp +ozora.hokkaido.jp +pippu.hokkaido.jp +rankoshi.hokkaido.jp +rebun.hokkaido.jp +rikubetsu.hokkaido.jp +rishiri.hokkaido.jp +rishirifuji.hokkaido.jp +saroma.hokkaido.jp +sarufutsu.hokkaido.jp +shakotan.hokkaido.jp +shari.hokkaido.jp +shibecha.hokkaido.jp +shibetsu.hokkaido.jp +shikabe.hokkaido.jp +shikaoi.hokkaido.jp +shimamaki.hokkaido.jp +shimizu.hokkaido.jp +shimokawa.hokkaido.jp +shinshinotsu.hokkaido.jp +shintoku.hokkaido.jp +shiranuka.hokkaido.jp +shiraoi.hokkaido.jp +shiriuchi.hokkaido.jp +sobetsu.hokkaido.jp +sunagawa.hokkaido.jp +taiki.hokkaido.jp +takasu.hokkaido.jp +takikawa.hokkaido.jp +takinoue.hokkaido.jp +teshikaga.hokkaido.jp +tobetsu.hokkaido.jp +tohma.hokkaido.jp +tomakomai.hokkaido.jp +tomari.hokkaido.jp +toya.hokkaido.jp +toyako.hokkaido.jp +toyotomi.hokkaido.jp +toyoura.hokkaido.jp +tsubetsu.hokkaido.jp +tsukigata.hokkaido.jp +urakawa.hokkaido.jp +urausu.hokkaido.jp +uryu.hokkaido.jp +utashinai.hokkaido.jp +wakkanai.hokkaido.jp +wassamu.hokkaido.jp +yakumo.hokkaido.jp +yoichi.hokkaido.jp +aioi.hyogo.jp +akashi.hyogo.jp +ako.hyogo.jp +amagasaki.hyogo.jp +aogaki.hyogo.jp +asago.hyogo.jp +ashiya.hyogo.jp +awaji.hyogo.jp +fukusaki.hyogo.jp +goshiki.hyogo.jp +harima.hyogo.jp +himeji.hyogo.jp +ichikawa.hyogo.jp +inagawa.hyogo.jp +itami.hyogo.jp +kakogawa.hyogo.jp +kamigori.hyogo.jp +kamikawa.hyogo.jp +kasai.hyogo.jp +kasuga.hyogo.jp +kawanishi.hyogo.jp +miki.hyogo.jp +minamiawaji.hyogo.jp +nishinomiya.hyogo.jp +nishiwaki.hyogo.jp +ono.hyogo.jp +sanda.hyogo.jp +sannan.hyogo.jp +sasayama.hyogo.jp +sayo.hyogo.jp +shingu.hyogo.jp +shinonsen.hyogo.jp +shiso.hyogo.jp +sumoto.hyogo.jp +taishi.hyogo.jp +taka.hyogo.jp +takarazuka.hyogo.jp +takasago.hyogo.jp +takino.hyogo.jp +tamba.hyogo.jp +tatsuno.hyogo.jp +toyooka.hyogo.jp +yabu.hyogo.jp +yashiro.hyogo.jp +yoka.hyogo.jp +yokawa.hyogo.jp +ami.ibaraki.jp +asahi.ibaraki.jp +bando.ibaraki.jp +chikusei.ibaraki.jp +daigo.ibaraki.jp +fujishiro.ibaraki.jp +hitachi.ibaraki.jp +hitachinaka.ibaraki.jp +hitachiomiya.ibaraki.jp +hitachiota.ibaraki.jp +ibaraki.ibaraki.jp +ina.ibaraki.jp +inashiki.ibaraki.jp +itako.ibaraki.jp +iwama.ibaraki.jp +joso.ibaraki.jp +kamisu.ibaraki.jp +kasama.ibaraki.jp +kashima.ibaraki.jp +kasumigaura.ibaraki.jp +koga.ibaraki.jp +miho.ibaraki.jp +mito.ibaraki.jp +moriya.ibaraki.jp +naka.ibaraki.jp +namegata.ibaraki.jp +oarai.ibaraki.jp +ogawa.ibaraki.jp +omitama.ibaraki.jp +ryugasaki.ibaraki.jp +sakai.ibaraki.jp +sakuragawa.ibaraki.jp +shimodate.ibaraki.jp +shimotsuma.ibaraki.jp +shirosato.ibaraki.jp +sowa.ibaraki.jp +suifu.ibaraki.jp +takahagi.ibaraki.jp +tamatsukuri.ibaraki.jp +tokai.ibaraki.jp +tomobe.ibaraki.jp +tone.ibaraki.jp +toride.ibaraki.jp +tsuchiura.ibaraki.jp +tsukuba.ibaraki.jp +uchihara.ibaraki.jp +ushiku.ibaraki.jp +yachiyo.ibaraki.jp +yamagata.ibaraki.jp +yawara.ibaraki.jp +yuki.ibaraki.jp +anamizu.ishikawa.jp +hakui.ishikawa.jp +hakusan.ishikawa.jp +kaga.ishikawa.jp +kahoku.ishikawa.jp +kanazawa.ishikawa.jp +kawakita.ishikawa.jp +komatsu.ishikawa.jp +nakanoto.ishikawa.jp +nanao.ishikawa.jp +nomi.ishikawa.jp +nonoichi.ishikawa.jp +noto.ishikawa.jp +shika.ishikawa.jp +suzu.ishikawa.jp +tsubata.ishikawa.jp +tsurugi.ishikawa.jp +uchinada.ishikawa.jp +wajima.ishikawa.jp +fudai.iwate.jp +fujisawa.iwate.jp +hanamaki.iwate.jp +hiraizumi.iwate.jp +hirono.iwate.jp +ichinohe.iwate.jp +ichinoseki.iwate.jp +iwaizumi.iwate.jp +iwate.iwate.jp +joboji.iwate.jp +kamaishi.iwate.jp +kanegasaki.iwate.jp +karumai.iwate.jp +kawai.iwate.jp +kitakami.iwate.jp +kuji.iwate.jp +kunohe.iwate.jp +kuzumaki.iwate.jp +miyako.iwate.jp +mizusawa.iwate.jp +morioka.iwate.jp +ninohe.iwate.jp +noda.iwate.jp +ofunato.iwate.jp +oshu.iwate.jp +otsuchi.iwate.jp +rikuzentakata.iwate.jp +shiwa.iwate.jp +shizukuishi.iwate.jp +sumita.iwate.jp +tanohata.iwate.jp +tono.iwate.jp +yahaba.iwate.jp +yamada.iwate.jp +ayagawa.kagawa.jp +higashikagawa.kagawa.jp +kanonji.kagawa.jp +kotohira.kagawa.jp +manno.kagawa.jp +marugame.kagawa.jp +mitoyo.kagawa.jp +naoshima.kagawa.jp +sanuki.kagawa.jp +tadotsu.kagawa.jp +takamatsu.kagawa.jp +tonosho.kagawa.jp +uchinomi.kagawa.jp +utazu.kagawa.jp +zentsuji.kagawa.jp +akune.kagoshima.jp +amami.kagoshima.jp +hioki.kagoshima.jp +isa.kagoshima.jp +isen.kagoshima.jp +izumi.kagoshima.jp +kagoshima.kagoshima.jp +kanoya.kagoshima.jp +kawanabe.kagoshima.jp +kinko.kagoshima.jp +kouyama.kagoshima.jp +makurazaki.kagoshima.jp +matsumoto.kagoshima.jp +minamitane.kagoshima.jp +nakatane.kagoshima.jp +nishinoomote.kagoshima.jp +satsumasendai.kagoshima.jp +soo.kagoshima.jp +tarumizu.kagoshima.jp +yusui.kagoshima.jp +aikawa.kanagawa.jp +atsugi.kanagawa.jp +ayase.kanagawa.jp +chigasaki.kanagawa.jp +ebina.kanagawa.jp +fujisawa.kanagawa.jp +hadano.kanagawa.jp +hakone.kanagawa.jp +hiratsuka.kanagawa.jp +isehara.kanagawa.jp +kaisei.kanagawa.jp +kamakura.kanagawa.jp +kiyokawa.kanagawa.jp +matsuda.kanagawa.jp +minamiashigara.kanagawa.jp +miura.kanagawa.jp +nakai.kanagawa.jp +ninomiya.kanagawa.jp +odawara.kanagawa.jp +oi.kanagawa.jp +oiso.kanagawa.jp +sagamihara.kanagawa.jp +samukawa.kanagawa.jp +tsukui.kanagawa.jp +yamakita.kanagawa.jp +yamato.kanagawa.jp +yokosuka.kanagawa.jp +yugawara.kanagawa.jp +zama.kanagawa.jp +zushi.kanagawa.jp +aki.kochi.jp +geisei.kochi.jp +hidaka.kochi.jp +higashitsuno.kochi.jp +ino.kochi.jp +kagami.kochi.jp +kami.kochi.jp +kitagawa.kochi.jp +kochi.kochi.jp +mihara.kochi.jp +motoyama.kochi.jp +muroto.kochi.jp +nahari.kochi.jp +nakamura.kochi.jp +nankoku.kochi.jp +nishitosa.kochi.jp +niyodogawa.kochi.jp +ochi.kochi.jp +okawa.kochi.jp +otoyo.kochi.jp +otsuki.kochi.jp +sakawa.kochi.jp +sukumo.kochi.jp +susaki.kochi.jp +tosa.kochi.jp +tosashimizu.kochi.jp +toyo.kochi.jp +tsuno.kochi.jp +umaji.kochi.jp +yasuda.kochi.jp +yusuhara.kochi.jp +amakusa.kumamoto.jp +arao.kumamoto.jp +aso.kumamoto.jp +choyo.kumamoto.jp +gyokuto.kumamoto.jp +kamiamakusa.kumamoto.jp +kikuchi.kumamoto.jp +kumamoto.kumamoto.jp +mashiki.kumamoto.jp +mifune.kumamoto.jp +minamata.kumamoto.jp +minamioguni.kumamoto.jp +nagasu.kumamoto.jp +nishihara.kumamoto.jp +oguni.kumamoto.jp +ozu.kumamoto.jp +sumoto.kumamoto.jp +takamori.kumamoto.jp +uki.kumamoto.jp +uto.kumamoto.jp +yamaga.kumamoto.jp +yamato.kumamoto.jp +yatsushiro.kumamoto.jp +ayabe.kyoto.jp +fukuchiyama.kyoto.jp +higashiyama.kyoto.jp +ide.kyoto.jp +ine.kyoto.jp +joyo.kyoto.jp +kameoka.kyoto.jp +kamo.kyoto.jp +kita.kyoto.jp +kizu.kyoto.jp +kumiyama.kyoto.jp +kyotamba.kyoto.jp +kyotanabe.kyoto.jp +kyotango.kyoto.jp +maizuru.kyoto.jp +minami.kyoto.jp +minamiyamashiro.kyoto.jp +miyazu.kyoto.jp +muko.kyoto.jp +nagaokakyo.kyoto.jp +nakagyo.kyoto.jp +nantan.kyoto.jp +oyamazaki.kyoto.jp +sakyo.kyoto.jp +seika.kyoto.jp +tanabe.kyoto.jp +uji.kyoto.jp +ujitawara.kyoto.jp +wazuka.kyoto.jp +yamashina.kyoto.jp +yawata.kyoto.jp +asahi.mie.jp +inabe.mie.jp +ise.mie.jp +kameyama.mie.jp +kawagoe.mie.jp +kiho.mie.jp +kisosaki.mie.jp +kiwa.mie.jp +komono.mie.jp +kumano.mie.jp +kuwana.mie.jp +matsusaka.mie.jp +meiwa.mie.jp +mihama.mie.jp +minamiise.mie.jp +misugi.mie.jp +miyama.mie.jp +nabari.mie.jp +shima.mie.jp +suzuka.mie.jp +tado.mie.jp +taiki.mie.jp +taki.mie.jp +tamaki.mie.jp +toba.mie.jp +tsu.mie.jp +udono.mie.jp +ureshino.mie.jp +watarai.mie.jp +yokkaichi.mie.jp +furukawa.miyagi.jp +higashimatsushima.miyagi.jp +ishinomaki.miyagi.jp +iwanuma.miyagi.jp +kakuda.miyagi.jp +kami.miyagi.jp +kawasaki.miyagi.jp +marumori.miyagi.jp +matsushima.miyagi.jp +minamisanriku.miyagi.jp +misato.miyagi.jp +murata.miyagi.jp +natori.miyagi.jp +ogawara.miyagi.jp +ohira.miyagi.jp +onagawa.miyagi.jp +osaki.miyagi.jp +rifu.miyagi.jp +semine.miyagi.jp +shibata.miyagi.jp +shichikashuku.miyagi.jp +shikama.miyagi.jp +shiogama.miyagi.jp +shiroishi.miyagi.jp +tagajo.miyagi.jp +taiwa.miyagi.jp +tome.miyagi.jp +tomiya.miyagi.jp +wakuya.miyagi.jp +watari.miyagi.jp +yamamoto.miyagi.jp +zao.miyagi.jp +aya.miyazaki.jp +ebino.miyazaki.jp +gokase.miyazaki.jp +hyuga.miyazaki.jp +kadogawa.miyazaki.jp +kawaminami.miyazaki.jp +kijo.miyazaki.jp +kitagawa.miyazaki.jp +kitakata.miyazaki.jp +kitaura.miyazaki.jp +kobayashi.miyazaki.jp +kunitomi.miyazaki.jp +kushima.miyazaki.jp +mimata.miyazaki.jp +miyakonojo.miyazaki.jp +miyazaki.miyazaki.jp +morotsuka.miyazaki.jp +nichinan.miyazaki.jp +nishimera.miyazaki.jp +nobeoka.miyazaki.jp +saito.miyazaki.jp +shiiba.miyazaki.jp +shintomi.miyazaki.jp +takaharu.miyazaki.jp +takanabe.miyazaki.jp +takazaki.miyazaki.jp +tsuno.miyazaki.jp +achi.nagano.jp +agematsu.nagano.jp +anan.nagano.jp +aoki.nagano.jp +asahi.nagano.jp +azumino.nagano.jp +chikuhoku.nagano.jp +chikuma.nagano.jp +chino.nagano.jp +fujimi.nagano.jp +hakuba.nagano.jp +hara.nagano.jp +hiraya.nagano.jp +iida.nagano.jp +iijima.nagano.jp +iiyama.nagano.jp +iizuna.nagano.jp +ikeda.nagano.jp +ikusaka.nagano.jp +ina.nagano.jp +karuizawa.nagano.jp +kawakami.nagano.jp +kiso.nagano.jp +kisofukushima.nagano.jp +kitaaiki.nagano.jp +komagane.nagano.jp +komoro.nagano.jp +matsukawa.nagano.jp +matsumoto.nagano.jp +miasa.nagano.jp +minamiaiki.nagano.jp +minamimaki.nagano.jp +minamiminowa.nagano.jp +minowa.nagano.jp +miyada.nagano.jp +miyota.nagano.jp +mochizuki.nagano.jp +nagano.nagano.jp +nagawa.nagano.jp +nagiso.nagano.jp +nakagawa.nagano.jp +nakano.nagano.jp +nozawaonsen.nagano.jp +obuse.nagano.jp +ogawa.nagano.jp +okaya.nagano.jp +omachi.nagano.jp +omi.nagano.jp +ookuwa.nagano.jp +ooshika.nagano.jp +otaki.nagano.jp +otari.nagano.jp +sakae.nagano.jp +sakaki.nagano.jp +saku.nagano.jp +sakuho.nagano.jp +shimosuwa.nagano.jp +shinanomachi.nagano.jp +shiojiri.nagano.jp +suwa.nagano.jp +suzaka.nagano.jp +takagi.nagano.jp +takamori.nagano.jp +takayama.nagano.jp +tateshina.nagano.jp +tatsuno.nagano.jp +togakushi.nagano.jp +togura.nagano.jp +tomi.nagano.jp +ueda.nagano.jp +wada.nagano.jp +yamagata.nagano.jp +yamanouchi.nagano.jp +yasaka.nagano.jp +yasuoka.nagano.jp +chijiwa.nagasaki.jp +futsu.nagasaki.jp +goto.nagasaki.jp +hasami.nagasaki.jp +hirado.nagasaki.jp +iki.nagasaki.jp +isahaya.nagasaki.jp +kawatana.nagasaki.jp +kuchinotsu.nagasaki.jp +matsuura.nagasaki.jp +nagasaki.nagasaki.jp +obama.nagasaki.jp +omura.nagasaki.jp +oseto.nagasaki.jp +saikai.nagasaki.jp +sasebo.nagasaki.jp +seihi.nagasaki.jp +shimabara.nagasaki.jp +shinkamigoto.nagasaki.jp +togitsu.nagasaki.jp +tsushima.nagasaki.jp +unzen.nagasaki.jp +ando.nara.jp +gose.nara.jp +heguri.nara.jp +higashiyoshino.nara.jp +ikaruga.nara.jp +ikoma.nara.jp +kamikitayama.nara.jp +kanmaki.nara.jp +kashiba.nara.jp +kashihara.nara.jp +katsuragi.nara.jp +kawai.nara.jp +kawakami.nara.jp +kawanishi.nara.jp +koryo.nara.jp +kurotaki.nara.jp +mitsue.nara.jp +miyake.nara.jp +nara.nara.jp +nosegawa.nara.jp +oji.nara.jp +ouda.nara.jp +oyodo.nara.jp +sakurai.nara.jp +sango.nara.jp +shimoichi.nara.jp +shimokitayama.nara.jp +shinjo.nara.jp +soni.nara.jp +takatori.nara.jp +tawaramoto.nara.jp +tenkawa.nara.jp +tenri.nara.jp +uda.nara.jp +yamatokoriyama.nara.jp +yamatotakada.nara.jp +yamazoe.nara.jp +yoshino.nara.jp +aga.niigata.jp +agano.niigata.jp +gosen.niigata.jp +itoigawa.niigata.jp +izumozaki.niigata.jp +joetsu.niigata.jp +kamo.niigata.jp +kariwa.niigata.jp +kashiwazaki.niigata.jp +minamiuonuma.niigata.jp +mitsuke.niigata.jp +muika.niigata.jp +murakami.niigata.jp +myoko.niigata.jp +nagaoka.niigata.jp +niigata.niigata.jp +ojiya.niigata.jp +omi.niigata.jp +sado.niigata.jp +sanjo.niigata.jp +seiro.niigata.jp +seirou.niigata.jp +sekikawa.niigata.jp +shibata.niigata.jp +tagami.niigata.jp +tainai.niigata.jp +tochio.niigata.jp +tokamachi.niigata.jp +tsubame.niigata.jp +tsunan.niigata.jp +uonuma.niigata.jp +yahiko.niigata.jp +yoita.niigata.jp +yuzawa.niigata.jp +beppu.oita.jp +bungoono.oita.jp +bungotakada.oita.jp +hasama.oita.jp +hiji.oita.jp +himeshima.oita.jp +hita.oita.jp +kamitsue.oita.jp +kokonoe.oita.jp +kuju.oita.jp +kunisaki.oita.jp +kusu.oita.jp +oita.oita.jp +saiki.oita.jp +taketa.oita.jp +tsukumi.oita.jp +usa.oita.jp +usuki.oita.jp +yufu.oita.jp +akaiwa.okayama.jp +asakuchi.okayama.jp +bizen.okayama.jp +hayashima.okayama.jp +ibara.okayama.jp +kagamino.okayama.jp +kasaoka.okayama.jp +kibichuo.okayama.jp +kumenan.okayama.jp +kurashiki.okayama.jp +maniwa.okayama.jp +misaki.okayama.jp +nagi.okayama.jp +niimi.okayama.jp +nishiawakura.okayama.jp +okayama.okayama.jp +satosho.okayama.jp +setouchi.okayama.jp +shinjo.okayama.jp +shoo.okayama.jp +soja.okayama.jp +takahashi.okayama.jp +tamano.okayama.jp +tsuyama.okayama.jp +wake.okayama.jp +yakage.okayama.jp +aguni.okinawa.jp +ginowan.okinawa.jp +ginoza.okinawa.jp +gushikami.okinawa.jp +haebaru.okinawa.jp +higashi.okinawa.jp +hirara.okinawa.jp +iheya.okinawa.jp +ishigaki.okinawa.jp +ishikawa.okinawa.jp +itoman.okinawa.jp +izena.okinawa.jp +kadena.okinawa.jp +kin.okinawa.jp +kitadaito.okinawa.jp +kitanakagusuku.okinawa.jp +kumejima.okinawa.jp +kunigami.okinawa.jp +minamidaito.okinawa.jp +motobu.okinawa.jp +nago.okinawa.jp +naha.okinawa.jp +nakagusuku.okinawa.jp +nakijin.okinawa.jp +nanjo.okinawa.jp +nishihara.okinawa.jp +ogimi.okinawa.jp +okinawa.okinawa.jp +onna.okinawa.jp +shimoji.okinawa.jp +taketomi.okinawa.jp +tarama.okinawa.jp +tokashiki.okinawa.jp +tomigusuku.okinawa.jp +tonaki.okinawa.jp +urasoe.okinawa.jp +uruma.okinawa.jp +yaese.okinawa.jp +yomitan.okinawa.jp +yonabaru.okinawa.jp +yonaguni.okinawa.jp +zamami.okinawa.jp +abeno.osaka.jp +chihayaakasaka.osaka.jp +chuo.osaka.jp +daito.osaka.jp +fujiidera.osaka.jp +habikino.osaka.jp +hannan.osaka.jp +higashiosaka.osaka.jp +higashisumiyoshi.osaka.jp +higashiyodogawa.osaka.jp +hirakata.osaka.jp +ibaraki.osaka.jp +ikeda.osaka.jp +izumi.osaka.jp +izumiotsu.osaka.jp +izumisano.osaka.jp +kadoma.osaka.jp +kaizuka.osaka.jp +kanan.osaka.jp +kashiwara.osaka.jp +katano.osaka.jp +kawachinagano.osaka.jp +kishiwada.osaka.jp +kita.osaka.jp +kumatori.osaka.jp +matsubara.osaka.jp +minato.osaka.jp +minoh.osaka.jp +misaki.osaka.jp +moriguchi.osaka.jp +neyagawa.osaka.jp +nishi.osaka.jp +nose.osaka.jp +osakasayama.osaka.jp +sakai.osaka.jp +sayama.osaka.jp +sennan.osaka.jp +settsu.osaka.jp +shijonawate.osaka.jp +shimamoto.osaka.jp +suita.osaka.jp +tadaoka.osaka.jp +taishi.osaka.jp +tajiri.osaka.jp +takaishi.osaka.jp +takatsuki.osaka.jp +tondabayashi.osaka.jp +toyonaka.osaka.jp +toyono.osaka.jp +yao.osaka.jp +ariake.saga.jp +arita.saga.jp +fukudomi.saga.jp +genkai.saga.jp +hamatama.saga.jp +hizen.saga.jp +imari.saga.jp +kamimine.saga.jp +kanzaki.saga.jp +karatsu.saga.jp +kashima.saga.jp +kitagata.saga.jp +kitahata.saga.jp +kiyama.saga.jp +kouhoku.saga.jp +kyuragi.saga.jp +nishiarita.saga.jp +ogi.saga.jp +omachi.saga.jp +ouchi.saga.jp +saga.saga.jp +shiroishi.saga.jp +taku.saga.jp +tara.saga.jp +tosu.saga.jp +yoshinogari.saga.jp +arakawa.saitama.jp +asaka.saitama.jp +chichibu.saitama.jp +fujimi.saitama.jp +fujimino.saitama.jp +fukaya.saitama.jp +hanno.saitama.jp +hanyu.saitama.jp +hasuda.saitama.jp +hatogaya.saitama.jp +hatoyama.saitama.jp +hidaka.saitama.jp +higashichichibu.saitama.jp +higashimatsuyama.saitama.jp +honjo.saitama.jp +ina.saitama.jp +iruma.saitama.jp +iwatsuki.saitama.jp +kamiizumi.saitama.jp +kamikawa.saitama.jp +kamisato.saitama.jp +kasukabe.saitama.jp +kawagoe.saitama.jp +kawaguchi.saitama.jp +kawajima.saitama.jp +kazo.saitama.jp +kitamoto.saitama.jp +koshigaya.saitama.jp +kounosu.saitama.jp +kuki.saitama.jp +kumagaya.saitama.jp +matsubushi.saitama.jp +minano.saitama.jp +misato.saitama.jp +miyashiro.saitama.jp +miyoshi.saitama.jp +moroyama.saitama.jp +nagatoro.saitama.jp +namegawa.saitama.jp +niiza.saitama.jp +ogano.saitama.jp +ogawa.saitama.jp +ogose.saitama.jp +okegawa.saitama.jp +omiya.saitama.jp +otaki.saitama.jp +ranzan.saitama.jp +ryokami.saitama.jp +saitama.saitama.jp +sakado.saitama.jp +satte.saitama.jp +sayama.saitama.jp +shiki.saitama.jp +shiraoka.saitama.jp +soka.saitama.jp +sugito.saitama.jp +toda.saitama.jp +tokigawa.saitama.jp +tokorozawa.saitama.jp +tsurugashima.saitama.jp +urawa.saitama.jp +warabi.saitama.jp +yashio.saitama.jp +yokoze.saitama.jp +yono.saitama.jp +yorii.saitama.jp +yoshida.saitama.jp +yoshikawa.saitama.jp +yoshimi.saitama.jp +aisho.shiga.jp +gamo.shiga.jp +higashiomi.shiga.jp +hikone.shiga.jp +koka.shiga.jp +konan.shiga.jp +kosei.shiga.jp +koto.shiga.jp +kusatsu.shiga.jp +maibara.shiga.jp +moriyama.shiga.jp +nagahama.shiga.jp +nishiazai.shiga.jp +notogawa.shiga.jp +omihachiman.shiga.jp +otsu.shiga.jp +ritto.shiga.jp +ryuoh.shiga.jp +takashima.shiga.jp +takatsuki.shiga.jp +torahime.shiga.jp +toyosato.shiga.jp +yasu.shiga.jp +akagi.shimane.jp +ama.shimane.jp +gotsu.shimane.jp +hamada.shimane.jp +higashiizumo.shimane.jp +hikawa.shimane.jp +hikimi.shimane.jp +izumo.shimane.jp +kakinoki.shimane.jp +masuda.shimane.jp +matsue.shimane.jp +misato.shimane.jp +nishinoshima.shimane.jp +ohda.shimane.jp +okinoshima.shimane.jp +okuizumo.shimane.jp +shimane.shimane.jp +tamayu.shimane.jp +tsuwano.shimane.jp +unnan.shimane.jp +yakumo.shimane.jp +yasugi.shimane.jp +yatsuka.shimane.jp +arai.shizuoka.jp +atami.shizuoka.jp +fuji.shizuoka.jp +fujieda.shizuoka.jp +fujikawa.shizuoka.jp +fujinomiya.shizuoka.jp +fukuroi.shizuoka.jp +gotemba.shizuoka.jp +haibara.shizuoka.jp +hamamatsu.shizuoka.jp +higashiizu.shizuoka.jp +ito.shizuoka.jp +iwata.shizuoka.jp +izu.shizuoka.jp +izunokuni.shizuoka.jp +kakegawa.shizuoka.jp +kannami.shizuoka.jp +kawanehon.shizuoka.jp +kawazu.shizuoka.jp +kikugawa.shizuoka.jp +kosai.shizuoka.jp +makinohara.shizuoka.jp +matsuzaki.shizuoka.jp +minamiizu.shizuoka.jp +mishima.shizuoka.jp +morimachi.shizuoka.jp +nishiizu.shizuoka.jp +numazu.shizuoka.jp +omaezaki.shizuoka.jp +shimada.shizuoka.jp +shimizu.shizuoka.jp +shimoda.shizuoka.jp +shizuoka.shizuoka.jp +susono.shizuoka.jp +yaizu.shizuoka.jp +yoshida.shizuoka.jp +ashikaga.tochigi.jp +bato.tochigi.jp +haga.tochigi.jp +ichikai.tochigi.jp +iwafune.tochigi.jp +kaminokawa.tochigi.jp +kanuma.tochigi.jp +karasuyama.tochigi.jp +kuroiso.tochigi.jp +mashiko.tochigi.jp +mibu.tochigi.jp +moka.tochigi.jp +motegi.tochigi.jp +nasu.tochigi.jp +nasushiobara.tochigi.jp +nikko.tochigi.jp +nishikata.tochigi.jp +nogi.tochigi.jp +ohira.tochigi.jp +ohtawara.tochigi.jp +oyama.tochigi.jp +sakura.tochigi.jp +sano.tochigi.jp +shimotsuke.tochigi.jp +shioya.tochigi.jp +takanezawa.tochigi.jp +tochigi.tochigi.jp +tsuga.tochigi.jp +ujiie.tochigi.jp +utsunomiya.tochigi.jp +yaita.tochigi.jp +aizumi.tokushima.jp +anan.tokushima.jp +ichiba.tokushima.jp +itano.tokushima.jp +kainan.tokushima.jp +komatsushima.tokushima.jp +matsushige.tokushima.jp +mima.tokushima.jp +minami.tokushima.jp +miyoshi.tokushima.jp +mugi.tokushima.jp +nakagawa.tokushima.jp +naruto.tokushima.jp +sanagochi.tokushima.jp +shishikui.tokushima.jp +tokushima.tokushima.jp +wajiki.tokushima.jp +adachi.tokyo.jp +akiruno.tokyo.jp +akishima.tokyo.jp +aogashima.tokyo.jp +arakawa.tokyo.jp +bunkyo.tokyo.jp +chiyoda.tokyo.jp +chofu.tokyo.jp +chuo.tokyo.jp +edogawa.tokyo.jp +fuchu.tokyo.jp +fussa.tokyo.jp +hachijo.tokyo.jp +hachioji.tokyo.jp +hamura.tokyo.jp +higashikurume.tokyo.jp +higashimurayama.tokyo.jp +higashiyamato.tokyo.jp +hino.tokyo.jp +hinode.tokyo.jp +hinohara.tokyo.jp +inagi.tokyo.jp +itabashi.tokyo.jp +katsushika.tokyo.jp +kita.tokyo.jp +kiyose.tokyo.jp +kodaira.tokyo.jp +koganei.tokyo.jp +kokubunji.tokyo.jp +komae.tokyo.jp +koto.tokyo.jp +kouzushima.tokyo.jp +kunitachi.tokyo.jp +machida.tokyo.jp +meguro.tokyo.jp +minato.tokyo.jp +mitaka.tokyo.jp +mizuho.tokyo.jp +musashimurayama.tokyo.jp +musashino.tokyo.jp +nakano.tokyo.jp +nerima.tokyo.jp +ogasawara.tokyo.jp +okutama.tokyo.jp +ome.tokyo.jp +oshima.tokyo.jp +ota.tokyo.jp +setagaya.tokyo.jp +shibuya.tokyo.jp +shinagawa.tokyo.jp +shinjuku.tokyo.jp +suginami.tokyo.jp +sumida.tokyo.jp +tachikawa.tokyo.jp +taito.tokyo.jp +tama.tokyo.jp +toshima.tokyo.jp +chizu.tottori.jp +hino.tottori.jp +kawahara.tottori.jp +koge.tottori.jp +kotoura.tottori.jp +misasa.tottori.jp +nanbu.tottori.jp +nichinan.tottori.jp +sakaiminato.tottori.jp +tottori.tottori.jp +wakasa.tottori.jp +yazu.tottori.jp +yonago.tottori.jp +asahi.toyama.jp +fuchu.toyama.jp +fukumitsu.toyama.jp +funahashi.toyama.jp +himi.toyama.jp +imizu.toyama.jp +inami.toyama.jp +johana.toyama.jp +kamiichi.toyama.jp +kurobe.toyama.jp +nakaniikawa.toyama.jp +namerikawa.toyama.jp +nanto.toyama.jp +nyuzen.toyama.jp +oyabe.toyama.jp +taira.toyama.jp +takaoka.toyama.jp +tateyama.toyama.jp +toga.toyama.jp +tonami.toyama.jp +toyama.toyama.jp +unazuki.toyama.jp +uozu.toyama.jp +yamada.toyama.jp +arida.wakayama.jp +aridagawa.wakayama.jp +gobo.wakayama.jp +hashimoto.wakayama.jp +hidaka.wakayama.jp +hirogawa.wakayama.jp +inami.wakayama.jp +iwade.wakayama.jp +kainan.wakayama.jp +kamitonda.wakayama.jp +katsuragi.wakayama.jp +kimino.wakayama.jp +kinokawa.wakayama.jp +kitayama.wakayama.jp +koya.wakayama.jp +koza.wakayama.jp +kozagawa.wakayama.jp +kudoyama.wakayama.jp +kushimoto.wakayama.jp +mihama.wakayama.jp +misato.wakayama.jp +nachikatsuura.wakayama.jp +shingu.wakayama.jp +shirahama.wakayama.jp +taiji.wakayama.jp +tanabe.wakayama.jp +wakayama.wakayama.jp +yuasa.wakayama.jp +yura.wakayama.jp +asahi.yamagata.jp +funagata.yamagata.jp +higashine.yamagata.jp +iide.yamagata.jp +kahoku.yamagata.jp +kaminoyama.yamagata.jp +kaneyama.yamagata.jp +kawanishi.yamagata.jp +mamurogawa.yamagata.jp +mikawa.yamagata.jp +murayama.yamagata.jp +nagai.yamagata.jp +nakayama.yamagata.jp +nanyo.yamagata.jp +nishikawa.yamagata.jp +obanazawa.yamagata.jp +oe.yamagata.jp +oguni.yamagata.jp +ohkura.yamagata.jp +oishida.yamagata.jp +sagae.yamagata.jp +sakata.yamagata.jp +sakegawa.yamagata.jp +shinjo.yamagata.jp +shirataka.yamagata.jp +shonai.yamagata.jp +takahata.yamagata.jp +tendo.yamagata.jp +tozawa.yamagata.jp +tsuruoka.yamagata.jp +yamagata.yamagata.jp +yamanobe.yamagata.jp +yonezawa.yamagata.jp +yuza.yamagata.jp +abu.yamaguchi.jp +hagi.yamaguchi.jp +hikari.yamaguchi.jp +hofu.yamaguchi.jp +iwakuni.yamaguchi.jp +kudamatsu.yamaguchi.jp +mitou.yamaguchi.jp +nagato.yamaguchi.jp +oshima.yamaguchi.jp +shimonoseki.yamaguchi.jp +shunan.yamaguchi.jp +tabuse.yamaguchi.jp +tokuyama.yamaguchi.jp +toyota.yamaguchi.jp +ube.yamaguchi.jp +yuu.yamaguchi.jp +chuo.yamanashi.jp +doshi.yamanashi.jp +fuefuki.yamanashi.jp +fujikawa.yamanashi.jp +fujikawaguchiko.yamanashi.jp +fujiyoshida.yamanashi.jp +hayakawa.yamanashi.jp +hokuto.yamanashi.jp +ichikawamisato.yamanashi.jp +kai.yamanashi.jp +kofu.yamanashi.jp +koshu.yamanashi.jp +kosuge.yamanashi.jp +minami-alps.yamanashi.jp +minobu.yamanashi.jp +nakamichi.yamanashi.jp +nanbu.yamanashi.jp +narusawa.yamanashi.jp +nirasaki.yamanashi.jp +nishikatsura.yamanashi.jp +oshino.yamanashi.jp +otsuki.yamanashi.jp +showa.yamanashi.jp +tabayama.yamanashi.jp +tsuru.yamanashi.jp +uenohara.yamanashi.jp +yamanakako.yamanashi.jp +yamanashi.yamanashi.jp + +// ke : http://www.kenic.or.ke/index.php/en/ke-domains/ke-domains +ke +ac.ke +co.ke +go.ke +info.ke +me.ke +mobi.ke +ne.ke +or.ke +sc.ke + +// kg : http://www.domain.kg/dmn_n.html +kg +org.kg +net.kg +com.kg +edu.kg +gov.kg +mil.kg + +// kh : http://www.mptc.gov.kh/dns_registration.htm +*.kh + +// ki : http://www.ki/dns/index.html +ki +edu.ki +biz.ki +net.ki +org.ki +gov.ki +info.ki +com.ki + +// km : https://en.wikipedia.org/wiki/.km +// http://www.domaine.km/documents/charte.doc +km +org.km +nom.km +gov.km +prd.km +tm.km +edu.km +mil.km +ass.km +com.km +// These are only mentioned as proposed suggestions at domaine.km, but +// https://en.wikipedia.org/wiki/.km says they're available for registration: +coop.km +asso.km +presse.km +medecin.km +notaires.km +pharmaciens.km +veterinaire.km +gouv.km + +// kn : https://en.wikipedia.org/wiki/.kn +// http://www.dot.kn/domainRules.html +kn +net.kn +org.kn +edu.kn +gov.kn + +// kp : http://www.kcce.kp/en_index.php +kp +com.kp +edu.kp +gov.kp +org.kp +rep.kp +tra.kp + +// kr : https://en.wikipedia.org/wiki/.kr +// see also: http://domain.nida.or.kr/eng/registration.jsp +kr +ac.kr +co.kr +es.kr +go.kr +hs.kr +kg.kr +mil.kr +ms.kr +ne.kr +or.kr +pe.kr +re.kr +sc.kr +// kr geographical names +busan.kr +chungbuk.kr +chungnam.kr +daegu.kr +daejeon.kr +gangwon.kr +gwangju.kr +gyeongbuk.kr +gyeonggi.kr +gyeongnam.kr +incheon.kr +jeju.kr +jeonbuk.kr +jeonnam.kr +seoul.kr +ulsan.kr + +// kw : https://www.nic.kw/policies/ +// Confirmed by registry +kw +com.kw +edu.kw +emb.kw +gov.kw +ind.kw +net.kw +org.kw + +// ky : http://www.icta.ky/da_ky_reg_dom.php +// Confirmed by registry 2008-06-17 +ky +edu.ky +gov.ky +com.ky +org.ky +net.ky + +// kz : https://en.wikipedia.org/wiki/.kz +// see also: http://www.nic.kz/rules/index.jsp +kz +org.kz +edu.kz +net.kz +gov.kz +mil.kz +com.kz + +// la : https://en.wikipedia.org/wiki/.la +// Submitted by registry +la +int.la +net.la +info.la +edu.la +gov.la +per.la +com.la +org.la + +// lb : https://en.wikipedia.org/wiki/.lb +// Submitted by registry +lb +com.lb +edu.lb +gov.lb +net.lb +org.lb + +// lc : https://en.wikipedia.org/wiki/.lc +// see also: http://www.nic.lc/rules.htm +lc +com.lc +net.lc +co.lc +org.lc +edu.lc +gov.lc + +// li : https://en.wikipedia.org/wiki/.li +li + +// lk : http://www.nic.lk/seclevpr.html +lk +gov.lk +sch.lk +net.lk +int.lk +com.lk +org.lk +edu.lk +ngo.lk +soc.lk +web.lk +ltd.lk +assn.lk +grp.lk +hotel.lk +ac.lk + +// lr : http://psg.com/dns/lr/lr.txt +// Submitted by registry +lr +com.lr +edu.lr +gov.lr +org.lr +net.lr + +// ls : https://en.wikipedia.org/wiki/.ls +ls +co.ls +org.ls + +// lt : https://en.wikipedia.org/wiki/.lt +lt +// gov.lt : http://www.gov.lt/index_en.php +gov.lt + +// lu : http://www.dns.lu/en/ +lu + +// lv : http://www.nic.lv/DNS/En/generic.php +lv +com.lv +edu.lv +gov.lv +org.lv +mil.lv +id.lv +net.lv +asn.lv +conf.lv + +// ly : http://www.nic.ly/regulations.php +ly +com.ly +net.ly +gov.ly +plc.ly +edu.ly +sch.ly +med.ly +org.ly +id.ly + +// ma : https://en.wikipedia.org/wiki/.ma +// http://www.anrt.ma/fr/admin/download/upload/file_fr782.pdf +ma +co.ma +net.ma +gov.ma +org.ma +ac.ma +press.ma + +// mc : http://www.nic.mc/ +mc +tm.mc +asso.mc + +// md : https://en.wikipedia.org/wiki/.md +md + +// me : https://en.wikipedia.org/wiki/.me +me +co.me +net.me +org.me +edu.me +ac.me +gov.me +its.me +priv.me + +// mg : http://nic.mg/nicmg/?page_id=39 +mg +org.mg +nom.mg +gov.mg +prd.mg +tm.mg +edu.mg +mil.mg +com.mg +co.mg + +// mh : https://en.wikipedia.org/wiki/.mh +mh + +// mil : https://en.wikipedia.org/wiki/.mil +mil + +// mk : https://en.wikipedia.org/wiki/.mk +// see also: http://dns.marnet.net.mk/postapka.php +mk +com.mk +org.mk +net.mk +edu.mk +gov.mk +inf.mk +name.mk + +// ml : http://www.gobin.info/domainname/ml-template.doc +// see also: https://en.wikipedia.org/wiki/.ml +ml +com.ml +edu.ml +gouv.ml +gov.ml +net.ml +org.ml +presse.ml + +// mm : https://en.wikipedia.org/wiki/.mm +*.mm + +// mn : https://en.wikipedia.org/wiki/.mn +mn +gov.mn +edu.mn +org.mn + +// mo : http://www.monic.net.mo/ +mo +com.mo +net.mo +org.mo +edu.mo +gov.mo + +// mobi : https://en.wikipedia.org/wiki/.mobi +mobi + +// mp : http://www.dot.mp/ +// Confirmed by registry 2008-06-17 +mp + +// mq : https://en.wikipedia.org/wiki/.mq +mq + +// mr : https://en.wikipedia.org/wiki/.mr +mr +gov.mr + +// ms : http://www.nic.ms/pdf/MS_Domain_Name_Rules.pdf +ms +com.ms +edu.ms +gov.ms +net.ms +org.ms + +// mt : https://www.nic.org.mt/go/policy +// Submitted by registry +mt +com.mt +edu.mt +net.mt +org.mt + +// mu : https://en.wikipedia.org/wiki/.mu +mu +com.mu +net.mu +org.mu +gov.mu +ac.mu +co.mu +or.mu + +// museum : http://about.museum/naming/ +// http://index.museum/ +museum +academy.museum +agriculture.museum +air.museum +airguard.museum +alabama.museum +alaska.museum +amber.museum +ambulance.museum +american.museum +americana.museum +americanantiques.museum +americanart.museum +amsterdam.museum +and.museum +annefrank.museum +anthro.museum +anthropology.museum +antiques.museum +aquarium.museum +arboretum.museum +archaeological.museum +archaeology.museum +architecture.museum +art.museum +artanddesign.museum +artcenter.museum +artdeco.museum +arteducation.museum +artgallery.museum +arts.museum +artsandcrafts.museum +asmatart.museum +assassination.museum +assisi.museum +association.museum +astronomy.museum +atlanta.museum +austin.museum +australia.museum +automotive.museum +aviation.museum +axis.museum +badajoz.museum +baghdad.museum +bahn.museum +bale.museum +baltimore.museum +barcelona.museum +baseball.museum +basel.museum +baths.museum +bauern.museum +beauxarts.museum +beeldengeluid.museum +bellevue.museum +bergbau.museum +berkeley.museum +berlin.museum +bern.museum +bible.museum +bilbao.museum +bill.museum +birdart.museum +birthplace.museum +bonn.museum +boston.museum +botanical.museum +botanicalgarden.museum +botanicgarden.museum +botany.museum +brandywinevalley.museum +brasil.museum +bristol.museum +british.museum +britishcolumbia.museum +broadcast.museum +brunel.museum +brussel.museum +brussels.museum +bruxelles.museum +building.museum +burghof.museum +bus.museum +bushey.museum +cadaques.museum +california.museum +cambridge.museum +can.museum +canada.museum +capebreton.museum +carrier.museum +cartoonart.museum +casadelamoneda.museum +castle.museum +castres.museum +celtic.museum +center.museum +chattanooga.museum +cheltenham.museum +chesapeakebay.museum +chicago.museum +children.museum +childrens.museum +childrensgarden.museum +chiropractic.museum +chocolate.museum +christiansburg.museum +cincinnati.museum +cinema.museum +circus.museum +civilisation.museum +civilization.museum +civilwar.museum +clinton.museum +clock.museum +coal.museum +coastaldefence.museum +cody.museum +coldwar.museum +collection.museum +colonialwilliamsburg.museum +coloradoplateau.museum +columbia.museum +columbus.museum +communication.museum +communications.museum +community.museum +computer.museum +computerhistory.museum +comunicações.museum +contemporary.museum +contemporaryart.museum +convent.museum +copenhagen.museum +corporation.museum +correios-e-telecomunicações.museum +corvette.museum +costume.museum +countryestate.museum +county.museum +crafts.museum +cranbrook.museum +creation.museum +cultural.museum +culturalcenter.museum +culture.museum +cyber.museum +cymru.museum +dali.museum +dallas.museum +database.museum +ddr.museum +decorativearts.museum +delaware.museum +delmenhorst.museum +denmark.museum +depot.museum +design.museum +detroit.museum +dinosaur.museum +discovery.museum +dolls.museum +donostia.museum +durham.museum +eastafrica.museum +eastcoast.museum +education.museum +educational.museum +egyptian.museum +eisenbahn.museum +elburg.museum +elvendrell.museum +embroidery.museum +encyclopedic.museum +england.museum +entomology.museum +environment.museum +environmentalconservation.museum +epilepsy.museum +essex.museum +estate.museum +ethnology.museum +exeter.museum +exhibition.museum +family.museum +farm.museum +farmequipment.museum +farmers.museum +farmstead.museum +field.museum +figueres.museum +filatelia.museum +film.museum +fineart.museum +finearts.museum +finland.museum +flanders.museum +florida.museum +force.museum +fortmissoula.museum +fortworth.museum +foundation.museum +francaise.museum +frankfurt.museum +franziskaner.museum +freemasonry.museum +freiburg.museum +fribourg.museum +frog.museum +fundacio.museum +furniture.museum +gallery.museum +garden.museum +gateway.museum +geelvinck.museum +gemological.museum +geology.museum +georgia.museum +giessen.museum +glas.museum +glass.museum +gorge.museum +grandrapids.museum +graz.museum +guernsey.museum +halloffame.museum +hamburg.museum +handson.museum +harvestcelebration.museum +hawaii.museum +health.museum +heimatunduhren.museum +hellas.museum +helsinki.museum +hembygdsforbund.museum +heritage.museum +histoire.museum +historical.museum +historicalsociety.museum +historichouses.museum +historisch.museum +historisches.museum +history.museum +historyofscience.museum +horology.museum +house.museum +humanities.museum +illustration.museum +imageandsound.museum +indian.museum +indiana.museum +indianapolis.museum +indianmarket.museum +intelligence.museum +interactive.museum +iraq.museum +iron.museum +isleofman.museum +jamison.museum +jefferson.museum +jerusalem.museum +jewelry.museum +jewish.museum +jewishart.museum +jfk.museum +journalism.museum +judaica.museum +judygarland.museum +juedisches.museum +juif.museum +karate.museum +karikatur.museum +kids.museum +koebenhavn.museum +koeln.museum +kunst.museum +kunstsammlung.museum +kunstunddesign.museum +labor.museum +labour.museum +lajolla.museum +lancashire.museum +landes.museum +lans.museum +läns.museum +larsson.museum +lewismiller.museum +lincoln.museum +linz.museum +living.museum +livinghistory.museum +localhistory.museum +london.museum +losangeles.museum +louvre.museum +loyalist.museum +lucerne.museum +luxembourg.museum +luzern.museum +mad.museum +madrid.museum +mallorca.museum +manchester.museum +mansion.museum +mansions.museum +manx.museum +marburg.museum +maritime.museum +maritimo.museum +maryland.museum +marylhurst.museum +media.museum +medical.museum +medizinhistorisches.museum +meeres.museum +memorial.museum +mesaverde.museum +michigan.museum +midatlantic.museum +military.museum +mill.museum +miners.museum +mining.museum +minnesota.museum +missile.museum +missoula.museum +modern.museum +moma.museum +money.museum +monmouth.museum +monticello.museum +montreal.museum +moscow.museum +motorcycle.museum +muenchen.museum +muenster.museum +mulhouse.museum +muncie.museum +museet.museum +museumcenter.museum +museumvereniging.museum +music.museum +national.museum +nationalfirearms.museum +nationalheritage.museum +nativeamerican.museum +naturalhistory.museum +naturalhistorymuseum.museum +naturalsciences.museum +nature.museum +naturhistorisches.museum +natuurwetenschappen.museum +naumburg.museum +naval.museum +nebraska.museum +neues.museum +newhampshire.museum +newjersey.museum +newmexico.museum +newport.museum +newspaper.museum +newyork.museum +niepce.museum +norfolk.museum +north.museum +nrw.museum +nuernberg.museum +nuremberg.museum +nyc.museum +nyny.museum +oceanographic.museum +oceanographique.museum +omaha.museum +online.museum +ontario.museum +openair.museum +oregon.museum +oregontrail.museum +otago.museum +oxford.museum +pacific.museum +paderborn.museum +palace.museum +paleo.museum +palmsprings.museum +panama.museum +paris.museum +pasadena.museum +pharmacy.museum +philadelphia.museum +philadelphiaarea.museum +philately.museum +phoenix.museum +photography.museum +pilots.museum +pittsburgh.museum +planetarium.museum +plantation.museum +plants.museum +plaza.museum +portal.museum +portland.museum +portlligat.museum +posts-and-telecommunications.museum +preservation.museum +presidio.museum +press.museum +project.museum +public.museum +pubol.museum +quebec.museum +railroad.museum +railway.museum +research.museum +resistance.museum +riodejaneiro.museum +rochester.museum +rockart.museum +roma.museum +russia.museum +saintlouis.museum +salem.museum +salvadordali.museum +salzburg.museum +sandiego.museum +sanfrancisco.museum +santabarbara.museum +santacruz.museum +santafe.museum +saskatchewan.museum +satx.museum +savannahga.museum +schlesisches.museum +schoenbrunn.museum +schokoladen.museum +school.museum +schweiz.museum +science.museum +scienceandhistory.museum +scienceandindustry.museum +sciencecenter.museum +sciencecenters.museum +science-fiction.museum +sciencehistory.museum +sciences.museum +sciencesnaturelles.museum +scotland.museum +seaport.museum +settlement.museum +settlers.museum +shell.museum +sherbrooke.museum +sibenik.museum +silk.museum +ski.museum +skole.museum +society.museum +sologne.museum +soundandvision.museum +southcarolina.museum +southwest.museum +space.museum +spy.museum +square.museum +stadt.museum +stalbans.museum +starnberg.museum +state.museum +stateofdelaware.museum +station.museum +steam.museum +steiermark.museum +stjohn.museum +stockholm.museum +stpetersburg.museum +stuttgart.museum +suisse.museum +surgeonshall.museum +surrey.museum +svizzera.museum +sweden.museum +sydney.museum +tank.museum +tcm.museum +technology.museum +telekommunikation.museum +television.museum +texas.museum +textile.museum +theater.museum +time.museum +timekeeping.museum +topology.museum +torino.museum +touch.museum +town.museum +transport.museum +tree.museum +trolley.museum +trust.museum +trustee.museum +uhren.museum +ulm.museum +undersea.museum +university.museum +usa.museum +usantiques.museum +usarts.museum +uscountryestate.museum +usculture.museum +usdecorativearts.museum +usgarden.museum +ushistory.museum +ushuaia.museum +uslivinghistory.museum +utah.museum +uvic.museum +valley.museum +vantaa.museum +versailles.museum +viking.museum +village.museum +virginia.museum +virtual.museum +virtuel.museum +vlaanderen.museum +volkenkunde.museum +wales.museum +wallonie.museum +war.museum +washingtondc.museum +watchandclock.museum +watch-and-clock.museum +western.museum +westfalen.museum +whaling.museum +wildlife.museum +williamsburg.museum +windmill.museum +workshop.museum +york.museum +yorkshire.museum +yosemite.museum +youth.museum +zoological.museum +zoology.museum +ירושלים.museum +иком.museum + +// mv : https://en.wikipedia.org/wiki/.mv +// "mv" included because, contra Wikipedia, google.mv exists. +mv +aero.mv +biz.mv +com.mv +coop.mv +edu.mv +gov.mv +info.mv +int.mv +mil.mv +museum.mv +name.mv +net.mv +org.mv +pro.mv + +// mw : http://www.registrar.mw/ +mw +ac.mw +biz.mw +co.mw +com.mw +coop.mw +edu.mw +gov.mw +int.mw +museum.mw +net.mw +org.mw + +// mx : http://www.nic.mx/ +// Submitted by registry +mx +com.mx +org.mx +gob.mx +edu.mx +net.mx + +// my : http://www.mynic.net.my/ +my +com.my +net.my +org.my +gov.my +edu.my +mil.my +name.my + +// mz : http://www.uem.mz/ +// Submitted by registry +mz +ac.mz +adv.mz +co.mz +edu.mz +gov.mz +mil.mz +net.mz +org.mz + +// na : http://www.na-nic.com.na/ +// http://www.info.na/domain/ +na +info.na +pro.na +name.na +school.na +or.na +dr.na +us.na +mx.na +ca.na +in.na +cc.na +tv.na +ws.na +mobi.na +co.na +com.na +org.na + +// name : has 2nd-level tlds, but there's no list of them +name + +// nc : http://www.cctld.nc/ +nc +asso.nc +nom.nc + +// ne : https://en.wikipedia.org/wiki/.ne +ne + +// net : https://en.wikipedia.org/wiki/.net +net + +// nf : https://en.wikipedia.org/wiki/.nf +nf +com.nf +net.nf +per.nf +rec.nf +web.nf +arts.nf +firm.nf +info.nf +other.nf +store.nf + +// ng : http://www.nira.org.ng/index.php/join-us/register-ng-domain/189-nira-slds +ng +com.ng +edu.ng +gov.ng +i.ng +mil.ng +mobi.ng +name.ng +net.ng +org.ng +sch.ng + +// ni : http://www.nic.ni/ +ni +ac.ni +biz.ni +co.ni +com.ni +edu.ni +gob.ni +in.ni +info.ni +int.ni +mil.ni +net.ni +nom.ni +org.ni +web.ni + +// nl : https://en.wikipedia.org/wiki/.nl +// https://www.sidn.nl/ +// ccTLD for the Netherlands +nl + +// BV.nl will be a registry for dutch BV's (besloten vennootschap) +bv.nl + +// no : http://www.norid.no/regelverk/index.en.html +// The Norwegian registry has declined to notify us of updates. The web pages +// referenced below are the official source of the data. There is also an +// announce mailing list: +// https://postlister.uninett.no/sympa/info/norid-diskusjon +no +// Norid generic domains : http://www.norid.no/regelverk/vedlegg-c.en.html +fhs.no +vgs.no +fylkesbibl.no +folkebibl.no +museum.no +idrett.no +priv.no +// Non-Norid generic domains : http://www.norid.no/regelverk/vedlegg-d.en.html +mil.no +stat.no +dep.no +kommune.no +herad.no +// no geographical names : http://www.norid.no/regelverk/vedlegg-b.en.html +// counties +aa.no +ah.no +bu.no +fm.no +hl.no +hm.no +jan-mayen.no +mr.no +nl.no +nt.no +of.no +ol.no +oslo.no +rl.no +sf.no +st.no +svalbard.no +tm.no +tr.no +va.no +vf.no +// primary and lower secondary schools per county +gs.aa.no +gs.ah.no +gs.bu.no +gs.fm.no +gs.hl.no +gs.hm.no +gs.jan-mayen.no +gs.mr.no +gs.nl.no +gs.nt.no +gs.of.no +gs.ol.no +gs.oslo.no +gs.rl.no +gs.sf.no +gs.st.no +gs.svalbard.no +gs.tm.no +gs.tr.no +gs.va.no +gs.vf.no +// cities +akrehamn.no +åkrehamn.no +algard.no +ålgård.no +arna.no +brumunddal.no +bryne.no +bronnoysund.no +brønnøysund.no +drobak.no +drøbak.no +egersund.no +fetsund.no +floro.no +florø.no +fredrikstad.no +hokksund.no +honefoss.no +hønefoss.no +jessheim.no +jorpeland.no +jørpeland.no +kirkenes.no +kopervik.no +krokstadelva.no +langevag.no +langevåg.no +leirvik.no +mjondalen.no +mjøndalen.no +mo-i-rana.no +mosjoen.no +mosjøen.no +nesoddtangen.no +orkanger.no +osoyro.no +osøyro.no +raholt.no +råholt.no +sandnessjoen.no +sandnessjøen.no +skedsmokorset.no +slattum.no +spjelkavik.no +stathelle.no +stavern.no +stjordalshalsen.no +stjørdalshalsen.no +tananger.no +tranby.no +vossevangen.no +// communities +afjord.no +åfjord.no +agdenes.no +al.no +ål.no +alesund.no +ålesund.no +alstahaug.no +alta.no +áltá.no +alaheadju.no +álaheadju.no +alvdal.no +amli.no +åmli.no +amot.no +åmot.no +andebu.no +andoy.no +andøy.no +andasuolo.no +ardal.no +årdal.no +aremark.no +arendal.no +ås.no +aseral.no +åseral.no +asker.no +askim.no +askvoll.no +askoy.no +askøy.no +asnes.no +åsnes.no +audnedaln.no +aukra.no +aure.no +aurland.no +aurskog-holand.no +aurskog-høland.no +austevoll.no +austrheim.no +averoy.no +averøy.no +balestrand.no +ballangen.no +balat.no +bálát.no +balsfjord.no +bahccavuotna.no +báhccavuotna.no +bamble.no +bardu.no +beardu.no +beiarn.no +bajddar.no +bájddar.no +baidar.no +báidár.no +berg.no +bergen.no +berlevag.no +berlevåg.no +bearalvahki.no +bearalváhki.no +bindal.no +birkenes.no +bjarkoy.no +bjarkøy.no +bjerkreim.no +bjugn.no +bodo.no +bodø.no +badaddja.no +bådåddjå.no +budejju.no +bokn.no +bremanger.no +bronnoy.no +brønnøy.no +bygland.no +bykle.no +barum.no +bærum.no +bo.telemark.no +bø.telemark.no +bo.nordland.no +bø.nordland.no +bievat.no +bievát.no +bomlo.no +bømlo.no +batsfjord.no +båtsfjord.no +bahcavuotna.no +báhcavuotna.no +dovre.no +drammen.no +drangedal.no +dyroy.no +dyrøy.no +donna.no +dønna.no +eid.no +eidfjord.no +eidsberg.no +eidskog.no +eidsvoll.no +eigersund.no +elverum.no +enebakk.no +engerdal.no +etne.no +etnedal.no +evenes.no +evenassi.no +evenášši.no +evje-og-hornnes.no +farsund.no +fauske.no +fuossko.no +fuoisku.no +fedje.no +fet.no +finnoy.no +finnøy.no +fitjar.no +fjaler.no +fjell.no +flakstad.no +flatanger.no +flekkefjord.no +flesberg.no +flora.no +fla.no +flå.no +folldal.no +forsand.no +fosnes.no +frei.no +frogn.no +froland.no +frosta.no +frana.no +fræna.no +froya.no +frøya.no +fusa.no +fyresdal.no +forde.no +førde.no +gamvik.no +gangaviika.no +gáŋgaviika.no +gaular.no +gausdal.no +gildeskal.no +gildeskål.no +giske.no +gjemnes.no +gjerdrum.no +gjerstad.no +gjesdal.no +gjovik.no +gjøvik.no +gloppen.no +gol.no +gran.no +grane.no +granvin.no +gratangen.no +grimstad.no +grong.no +kraanghke.no +kråanghke.no +grue.no +gulen.no +hadsel.no +halden.no +halsa.no +hamar.no +hamaroy.no +habmer.no +hábmer.no +hapmir.no +hápmir.no +hammerfest.no +hammarfeasta.no +hámmárfeasta.no +haram.no +hareid.no +harstad.no +hasvik.no +aknoluokta.no +ákŋoluokta.no +hattfjelldal.no +aarborte.no +haugesund.no +hemne.no +hemnes.no +hemsedal.no +heroy.more-og-romsdal.no +herøy.møre-og-romsdal.no +heroy.nordland.no +herøy.nordland.no +hitra.no +hjartdal.no +hjelmeland.no +hobol.no +hobøl.no +hof.no +hol.no +hole.no +holmestrand.no +holtalen.no +holtålen.no +hornindal.no +horten.no +hurdal.no +hurum.no +hvaler.no +hyllestad.no +hagebostad.no +hægebostad.no +hoyanger.no +høyanger.no +hoylandet.no +høylandet.no +ha.no +hå.no +ibestad.no +inderoy.no +inderøy.no +iveland.no +jevnaker.no +jondal.no +jolster.no +jølster.no +karasjok.no +karasjohka.no +kárášjohka.no +karlsoy.no +galsa.no +gálsá.no +karmoy.no +karmøy.no +kautokeino.no +guovdageaidnu.no +klepp.no +klabu.no +klæbu.no +kongsberg.no +kongsvinger.no +kragero.no +kragerø.no +kristiansand.no +kristiansund.no +krodsherad.no +krødsherad.no +kvalsund.no +rahkkeravju.no +ráhkkerávju.no +kvam.no +kvinesdal.no +kvinnherad.no +kviteseid.no +kvitsoy.no +kvitsøy.no +kvafjord.no +kvæfjord.no +giehtavuoatna.no +kvanangen.no +kvænangen.no +navuotna.no +návuotna.no +kafjord.no +kåfjord.no +gaivuotna.no +gáivuotna.no +larvik.no +lavangen.no +lavagis.no +loabat.no +loabát.no +lebesby.no +davvesiida.no +leikanger.no +leirfjord.no +leka.no +leksvik.no +lenvik.no +leangaviika.no +leaŋgaviika.no +lesja.no +levanger.no +lier.no +lierne.no +lillehammer.no +lillesand.no +lindesnes.no +lindas.no +lindås.no +lom.no +loppa.no +lahppi.no +láhppi.no +lund.no +lunner.no +luroy.no +lurøy.no +luster.no +lyngdal.no +lyngen.no +ivgu.no +lardal.no +lerdal.no +lærdal.no +lodingen.no +lødingen.no +lorenskog.no +lørenskog.no +loten.no +løten.no +malvik.no +masoy.no +måsøy.no +muosat.no +muosát.no +mandal.no +marker.no +marnardal.no +masfjorden.no +meland.no +meldal.no +melhus.no +meloy.no +meløy.no +meraker.no +meråker.no +moareke.no +moåreke.no +midsund.no +midtre-gauldal.no +modalen.no +modum.no +molde.no +moskenes.no +moss.no +mosvik.no +malselv.no +målselv.no +malatvuopmi.no +málatvuopmi.no +namdalseid.no +aejrie.no +namsos.no +namsskogan.no +naamesjevuemie.no +nååmesjevuemie.no +laakesvuemie.no +nannestad.no +narvik.no +narviika.no +naustdal.no +nedre-eiker.no +nes.akershus.no +nes.buskerud.no +nesna.no +nesodden.no +nesseby.no +unjarga.no +unjárga.no +nesset.no +nissedal.no +nittedal.no +nord-aurdal.no +nord-fron.no +nord-odal.no +norddal.no +nordkapp.no +davvenjarga.no +davvenjárga.no +nordre-land.no +nordreisa.no +raisa.no +ráisa.no +nore-og-uvdal.no +notodden.no +naroy.no +nærøy.no +notteroy.no +nøtterøy.no +odda.no +oksnes.no +øksnes.no +oppdal.no +oppegard.no +oppegård.no +orkdal.no +orland.no +ørland.no +orskog.no +ørskog.no +orsta.no +ørsta.no +os.hedmark.no +os.hordaland.no +osen.no +osteroy.no +osterøy.no +ostre-toten.no +østre-toten.no +overhalla.no +ovre-eiker.no +øvre-eiker.no +oyer.no +øyer.no +oygarden.no +øygarden.no +oystre-slidre.no +øystre-slidre.no +porsanger.no +porsangu.no +porsáŋgu.no +porsgrunn.no +radoy.no +radøy.no +rakkestad.no +rana.no +ruovat.no +randaberg.no +rauma.no +rendalen.no +rennebu.no +rennesoy.no +rennesøy.no +rindal.no +ringebu.no +ringerike.no +ringsaker.no +rissa.no +risor.no +risør.no +roan.no +rollag.no +rygge.no +ralingen.no +rælingen.no +rodoy.no +rødøy.no +romskog.no +rømskog.no +roros.no +røros.no +rost.no +røst.no +royken.no +røyken.no +royrvik.no +røyrvik.no +rade.no +råde.no +salangen.no +siellak.no +saltdal.no +salat.no +sálát.no +sálat.no +samnanger.no +sande.more-og-romsdal.no +sande.møre-og-romsdal.no +sande.vestfold.no +sandefjord.no +sandnes.no +sandoy.no +sandøy.no +sarpsborg.no +sauda.no +sauherad.no +sel.no +selbu.no +selje.no +seljord.no +sigdal.no +siljan.no +sirdal.no +skaun.no +skedsmo.no +ski.no +skien.no +skiptvet.no +skjervoy.no +skjervøy.no +skierva.no +skiervá.no +skjak.no +skjåk.no +skodje.no +skanland.no +skånland.no +skanit.no +skánit.no +smola.no +smøla.no +snillfjord.no +snasa.no +snåsa.no +snoasa.no +snaase.no +snåase.no +sogndal.no +sokndal.no +sola.no +solund.no +songdalen.no +sortland.no +spydeberg.no +stange.no +stavanger.no +steigen.no +steinkjer.no +stjordal.no +stjørdal.no +stokke.no +stor-elvdal.no +stord.no +stordal.no +storfjord.no +omasvuotna.no +strand.no +stranda.no +stryn.no +sula.no +suldal.no +sund.no +sunndal.no +surnadal.no +sveio.no +svelvik.no +sykkylven.no +sogne.no +søgne.no +somna.no +sømna.no +sondre-land.no +søndre-land.no +sor-aurdal.no +sør-aurdal.no +sor-fron.no +sør-fron.no +sor-odal.no +sør-odal.no +sor-varanger.no +sør-varanger.no +matta-varjjat.no +mátta-várjjat.no +sorfold.no +sørfold.no +sorreisa.no +sørreisa.no +sorum.no +sørum.no +tana.no +deatnu.no +time.no +tingvoll.no +tinn.no +tjeldsund.no +dielddanuorri.no +tjome.no +tjøme.no +tokke.no +tolga.no +torsken.no +tranoy.no +tranøy.no +tromso.no +tromsø.no +tromsa.no +romsa.no +trondheim.no +troandin.no +trysil.no +trana.no +træna.no +trogstad.no +trøgstad.no +tvedestrand.no +tydal.no +tynset.no +tysfjord.no +divtasvuodna.no +divttasvuotna.no +tysnes.no +tysvar.no +tysvær.no +tonsberg.no +tønsberg.no +ullensaker.no +ullensvang.no +ulvik.no +utsira.no +vadso.no +vadsø.no +cahcesuolo.no +čáhcesuolo.no +vaksdal.no +valle.no +vang.no +vanylven.no +vardo.no +vardø.no +varggat.no +várggát.no +vefsn.no +vaapste.no +vega.no +vegarshei.no +vegårshei.no +vennesla.no +verdal.no +verran.no +vestby.no +vestnes.no +vestre-slidre.no +vestre-toten.no +vestvagoy.no +vestvågøy.no +vevelstad.no +vik.no +vikna.no +vindafjord.no +volda.no +voss.no +varoy.no +værøy.no +vagan.no +vågan.no +voagat.no +vagsoy.no +vågsøy.no +vaga.no +vågå.no +valer.ostfold.no +våler.østfold.no +valer.hedmark.no +våler.hedmark.no + +// np : http://www.mos.com.np/register.html +*.np + +// nr : http://cenpac.net.nr/dns/index.html +// Submitted by registry +nr +biz.nr +info.nr +gov.nr +edu.nr +org.nr +net.nr +com.nr + +// nu : https://en.wikipedia.org/wiki/.nu +nu + +// nz : https://en.wikipedia.org/wiki/.nz +// Submitted by registry +nz +ac.nz +co.nz +cri.nz +geek.nz +gen.nz +govt.nz +health.nz +iwi.nz +kiwi.nz +maori.nz +mil.nz +māori.nz +net.nz +org.nz +parliament.nz +school.nz + +// om : https://en.wikipedia.org/wiki/.om +om +co.om +com.om +edu.om +gov.om +med.om +museum.om +net.om +org.om +pro.om + +// onion : https://tools.ietf.org/html/rfc7686 +onion + +// org : https://en.wikipedia.org/wiki/.org +org + +// pa : http://www.nic.pa/ +// Some additional second level "domains" resolve directly as hostnames, such as +// pannet.pa, so we add a rule for "pa". +pa +ac.pa +gob.pa +com.pa +org.pa +sld.pa +edu.pa +net.pa +ing.pa +abo.pa +med.pa +nom.pa + +// pe : https://www.nic.pe/InformeFinalComision.pdf +pe +edu.pe +gob.pe +nom.pe +mil.pe +org.pe +com.pe +net.pe + +// pf : http://www.gobin.info/domainname/formulaire-pf.pdf +pf +com.pf +org.pf +edu.pf + +// pg : https://en.wikipedia.org/wiki/.pg +*.pg + +// ph : http://www.domains.ph/FAQ2.asp +// Submitted by registry +ph +com.ph +net.ph +org.ph +gov.ph +edu.ph +ngo.ph +mil.ph +i.ph + +// pk : http://pk5.pknic.net.pk/pk5/msgNamepk.PK +pk +com.pk +net.pk +edu.pk +org.pk +fam.pk +biz.pk +web.pk +gov.pk +gob.pk +gok.pk +gon.pk +gop.pk +gos.pk +info.pk + +// pl http://www.dns.pl/english/index.html +// Submitted by registry +pl +com.pl +net.pl +org.pl +// pl functional domains (http://www.dns.pl/english/index.html) +aid.pl +agro.pl +atm.pl +auto.pl +biz.pl +edu.pl +gmina.pl +gsm.pl +info.pl +mail.pl +miasta.pl +media.pl +mil.pl +nieruchomosci.pl +nom.pl +pc.pl +powiat.pl +priv.pl +realestate.pl +rel.pl +sex.pl +shop.pl +sklep.pl +sos.pl +szkola.pl +targi.pl +tm.pl +tourism.pl +travel.pl +turystyka.pl +// Government domains +gov.pl +ap.gov.pl +ic.gov.pl +is.gov.pl +us.gov.pl +kmpsp.gov.pl +kppsp.gov.pl +kwpsp.gov.pl +psp.gov.pl +wskr.gov.pl +kwp.gov.pl +mw.gov.pl +ug.gov.pl +um.gov.pl +umig.gov.pl +ugim.gov.pl +upow.gov.pl +uw.gov.pl +starostwo.gov.pl +pa.gov.pl +po.gov.pl +psse.gov.pl +pup.gov.pl +rzgw.gov.pl +sa.gov.pl +so.gov.pl +sr.gov.pl +wsa.gov.pl +sko.gov.pl +uzs.gov.pl +wiih.gov.pl +winb.gov.pl +pinb.gov.pl +wios.gov.pl +witd.gov.pl +wzmiuw.gov.pl +piw.gov.pl +wiw.gov.pl +griw.gov.pl +wif.gov.pl +oum.gov.pl +sdn.gov.pl +zp.gov.pl +uppo.gov.pl +mup.gov.pl +wuoz.gov.pl +konsulat.gov.pl +oirm.gov.pl +// pl regional domains (http://www.dns.pl/english/index.html) +augustow.pl +babia-gora.pl +bedzin.pl +beskidy.pl +bialowieza.pl +bialystok.pl +bielawa.pl +bieszczady.pl +boleslawiec.pl +bydgoszcz.pl +bytom.pl +cieszyn.pl +czeladz.pl +czest.pl +dlugoleka.pl +elblag.pl +elk.pl +glogow.pl +gniezno.pl +gorlice.pl +grajewo.pl +ilawa.pl +jaworzno.pl +jelenia-gora.pl +jgora.pl +kalisz.pl +kazimierz-dolny.pl +karpacz.pl +kartuzy.pl +kaszuby.pl +katowice.pl +kepno.pl +ketrzyn.pl +klodzko.pl +kobierzyce.pl +kolobrzeg.pl +konin.pl +konskowola.pl +kutno.pl +lapy.pl +lebork.pl +legnica.pl +lezajsk.pl +limanowa.pl +lomza.pl +lowicz.pl +lubin.pl +lukow.pl +malbork.pl +malopolska.pl +mazowsze.pl +mazury.pl +mielec.pl +mielno.pl +mragowo.pl +naklo.pl +nowaruda.pl +nysa.pl +olawa.pl +olecko.pl +olkusz.pl +olsztyn.pl +opoczno.pl +opole.pl +ostroda.pl +ostroleka.pl +ostrowiec.pl +ostrowwlkp.pl +pila.pl +pisz.pl +podhale.pl +podlasie.pl +polkowice.pl +pomorze.pl +pomorskie.pl +prochowice.pl +pruszkow.pl +przeworsk.pl +pulawy.pl +radom.pl +rawa-maz.pl +rybnik.pl +rzeszow.pl +sanok.pl +sejny.pl +slask.pl +slupsk.pl +sosnowiec.pl +stalowa-wola.pl +skoczow.pl +starachowice.pl +stargard.pl +suwalki.pl +swidnica.pl +swiebodzin.pl +swinoujscie.pl +szczecin.pl +szczytno.pl +tarnobrzeg.pl +tgory.pl +turek.pl +tychy.pl +ustka.pl +walbrzych.pl +warmia.pl +warszawa.pl +waw.pl +wegrow.pl +wielun.pl +wlocl.pl +wloclawek.pl +wodzislaw.pl +wolomin.pl +wroclaw.pl +zachpomor.pl +zagan.pl +zarow.pl +zgora.pl +zgorzelec.pl + +// pm : http://www.afnic.fr/medias/documents/AFNIC-naming-policy2012.pdf +pm + +// pn : http://www.government.pn/PnRegistry/policies.htm +pn +gov.pn +co.pn +org.pn +edu.pn +net.pn + +// post : https://en.wikipedia.org/wiki/.post +post + +// pr : http://www.nic.pr/index.asp?f=1 +pr +com.pr +net.pr +org.pr +gov.pr +edu.pr +isla.pr +pro.pr +biz.pr +info.pr +name.pr +// these aren't mentioned on nic.pr, but on https://en.wikipedia.org/wiki/.pr +est.pr +prof.pr +ac.pr + +// pro : http://registry.pro/get-pro +pro +aaa.pro +aca.pro +acct.pro +avocat.pro +bar.pro +cpa.pro +eng.pro +jur.pro +law.pro +med.pro +recht.pro + +// ps : https://en.wikipedia.org/wiki/.ps +// http://www.nic.ps/registration/policy.html#reg +ps +edu.ps +gov.ps +sec.ps +plo.ps +com.ps +org.ps +net.ps + +// pt : http://online.dns.pt/dns/start_dns +pt +net.pt +gov.pt +org.pt +edu.pt +int.pt +publ.pt +com.pt +nome.pt + +// pw : https://en.wikipedia.org/wiki/.pw +pw +co.pw +ne.pw +or.pw +ed.pw +go.pw +belau.pw + +// py : http://www.nic.py/pautas.html#seccion_9 +// Submitted by registry +py +com.py +coop.py +edu.py +gov.py +mil.py +net.py +org.py + +// qa : http://domains.qa/en/ +qa +com.qa +edu.qa +gov.qa +mil.qa +name.qa +net.qa +org.qa +sch.qa + +// re : http://www.afnic.re/obtenir/chartes/nommage-re/annexe-descriptifs +re +asso.re +com.re +nom.re + +// ro : http://www.rotld.ro/ +ro +arts.ro +com.ro +firm.ro +info.ro +nom.ro +nt.ro +org.ro +rec.ro +store.ro +tm.ro +www.ro + +// rs : https://www.rnids.rs/en/domains/national-domains +rs +ac.rs +co.rs +edu.rs +gov.rs +in.rs +org.rs + +// ru : https://cctld.ru/en/domains/domens_ru/reserved/ +ru +ac.ru +edu.ru +gov.ru +int.ru +mil.ru +test.ru + +// rw : http://www.nic.rw/cgi-bin/policy.pl +rw +gov.rw +net.rw +edu.rw +ac.rw +com.rw +co.rw +int.rw +mil.rw +gouv.rw + +// sa : http://www.nic.net.sa/ +sa +com.sa +net.sa +org.sa +gov.sa +med.sa +pub.sa +edu.sa +sch.sa + +// sb : http://www.sbnic.net.sb/ +// Submitted by registry +sb +com.sb +edu.sb +gov.sb +net.sb +org.sb + +// sc : http://www.nic.sc/ +sc +com.sc +gov.sc +net.sc +org.sc +edu.sc + +// sd : http://www.isoc.sd/sudanic.isoc.sd/billing_pricing.htm +// Submitted by registry +sd +com.sd +net.sd +org.sd +edu.sd +med.sd +tv.sd +gov.sd +info.sd + +// se : https://en.wikipedia.org/wiki/.se +// Submitted by registry +se +a.se +ac.se +b.se +bd.se +brand.se +c.se +d.se +e.se +f.se +fh.se +fhsk.se +fhv.se +g.se +h.se +i.se +k.se +komforb.se +kommunalforbund.se +komvux.se +l.se +lanbib.se +m.se +n.se +naturbruksgymn.se +o.se +org.se +p.se +parti.se +pp.se +press.se +r.se +s.se +t.se +tm.se +u.se +w.se +x.se +y.se +z.se + +// sg : http://www.nic.net.sg/page/registration-policies-procedures-and-guidelines +sg +com.sg +net.sg +org.sg +gov.sg +edu.sg +per.sg + +// sh : http://www.nic.sh/registrar.html +sh +com.sh +net.sh +gov.sh +org.sh +mil.sh + +// si : https://en.wikipedia.org/wiki/.si +si + +// sj : No registrations at this time. +// Submitted by registry +sj + +// sk : https://en.wikipedia.org/wiki/.sk +// list of 2nd level domains ? +sk + +// sl : http://www.nic.sl +// Submitted by registry +sl +com.sl +net.sl +edu.sl +gov.sl +org.sl + +// sm : https://en.wikipedia.org/wiki/.sm +sm + +// sn : https://en.wikipedia.org/wiki/.sn +sn +art.sn +com.sn +edu.sn +gouv.sn +org.sn +perso.sn +univ.sn + +// so : http://www.soregistry.com/ +so +com.so +net.so +org.so + +// sr : https://en.wikipedia.org/wiki/.sr +sr + +// st : http://www.nic.st/html/policyrules/ +st +co.st +com.st +consulado.st +edu.st +embaixada.st +gov.st +mil.st +net.st +org.st +principe.st +saotome.st +store.st + +// su : https://en.wikipedia.org/wiki/.su +su + +// sv : http://www.svnet.org.sv/niveldos.pdf +sv +com.sv +edu.sv +gob.sv +org.sv +red.sv + +// sx : https://en.wikipedia.org/wiki/.sx +// Submitted by registry +sx +gov.sx + +// sy : https://en.wikipedia.org/wiki/.sy +// see also: http://www.gobin.info/domainname/sy.doc +sy +edu.sy +gov.sy +net.sy +mil.sy +com.sy +org.sy + +// sz : https://en.wikipedia.org/wiki/.sz +// http://www.sispa.org.sz/ +sz +co.sz +ac.sz +org.sz + +// tc : https://en.wikipedia.org/wiki/.tc +tc + +// td : https://en.wikipedia.org/wiki/.td +td + +// tel: https://en.wikipedia.org/wiki/.tel +// http://www.telnic.org/ +tel + +// tf : https://en.wikipedia.org/wiki/.tf +tf + +// tg : https://en.wikipedia.org/wiki/.tg +// http://www.nic.tg/ +tg + +// th : https://en.wikipedia.org/wiki/.th +// Submitted by registry +th +ac.th +co.th +go.th +in.th +mi.th +net.th +or.th + +// tj : http://www.nic.tj/policy.html +tj +ac.tj +biz.tj +co.tj +com.tj +edu.tj +go.tj +gov.tj +int.tj +mil.tj +name.tj +net.tj +nic.tj +org.tj +test.tj +web.tj + +// tk : https://en.wikipedia.org/wiki/.tk +tk + +// tl : https://en.wikipedia.org/wiki/.tl +tl +gov.tl + +// tm : http://www.nic.tm/local.html +tm +com.tm +co.tm +org.tm +net.tm +nom.tm +gov.tm +mil.tm +edu.tm + +// tn : https://en.wikipedia.org/wiki/.tn +// http://whois.ati.tn/ +tn +com.tn +ens.tn +fin.tn +gov.tn +ind.tn +intl.tn +nat.tn +net.tn +org.tn +info.tn +perso.tn +tourism.tn +edunet.tn +rnrt.tn +rns.tn +rnu.tn +mincom.tn +agrinet.tn +defense.tn +turen.tn + +// to : https://en.wikipedia.org/wiki/.to +// Submitted by registry +to +com.to +gov.to +net.to +org.to +edu.to +mil.to + +// subTLDs: https://www.nic.tr/forms/eng/policies.pdf +// and: https://www.nic.tr/forms/politikalar.pdf +// Submitted by +tr +com.tr +info.tr +biz.tr +net.tr +org.tr +web.tr +gen.tr +tv.tr +av.tr +dr.tr +bbs.tr +name.tr +tel.tr +gov.tr +bel.tr +pol.tr +mil.tr +k12.tr +edu.tr +kep.tr + +// Used by Northern Cyprus +nc.tr + +// Used by government agencies of Northern Cyprus +gov.nc.tr + +// tt : http://www.nic.tt/ +tt +co.tt +com.tt +org.tt +net.tt +biz.tt +info.tt +pro.tt +int.tt +coop.tt +jobs.tt +mobi.tt +travel.tt +museum.tt +aero.tt +name.tt +gov.tt +edu.tt + +// tv : https://en.wikipedia.org/wiki/.tv +// Not listing any 2LDs as reserved since none seem to exist in practice, +// Wikipedia notwithstanding. +tv + +// tw : https://en.wikipedia.org/wiki/.tw +tw +edu.tw +gov.tw +mil.tw +com.tw +net.tw +org.tw +idv.tw +game.tw +ebiz.tw +club.tw +網路.tw +組織.tw +商業.tw + +// tz : http://www.tznic.or.tz/index.php/domains +// Submitted by registry +tz +ac.tz +co.tz +go.tz +hotel.tz +info.tz +me.tz +mil.tz +mobi.tz +ne.tz +or.tz +sc.tz +tv.tz + +// ua : https://hostmaster.ua/policy/?ua +// Submitted by registry +ua +// ua 2LD +com.ua +edu.ua +gov.ua +in.ua +net.ua +org.ua +// ua geographic names +// https://hostmaster.ua/2ld/ +cherkassy.ua +cherkasy.ua +chernigov.ua +chernihiv.ua +chernivtsi.ua +chernovtsy.ua +ck.ua +cn.ua +cr.ua +crimea.ua +cv.ua +dn.ua +dnepropetrovsk.ua +dnipropetrovsk.ua +dominic.ua +donetsk.ua +dp.ua +if.ua +ivano-frankivsk.ua +kh.ua +kharkiv.ua +kharkov.ua +kherson.ua +khmelnitskiy.ua +khmelnytskyi.ua +kiev.ua +kirovograd.ua +km.ua +kr.ua +krym.ua +ks.ua +kv.ua +kyiv.ua +lg.ua +lt.ua +lugansk.ua +lutsk.ua +lv.ua +lviv.ua +mk.ua +mykolaiv.ua +nikolaev.ua +od.ua +odesa.ua +odessa.ua +pl.ua +poltava.ua +rivne.ua +rovno.ua +rv.ua +sb.ua +sebastopol.ua +sevastopol.ua +sm.ua +sumy.ua +te.ua +ternopil.ua +uz.ua +uzhgorod.ua +vinnica.ua +vinnytsia.ua +vn.ua +volyn.ua +yalta.ua +zaporizhzhe.ua +zaporizhzhia.ua +zhitomir.ua +zhytomyr.ua +zp.ua +zt.ua + +// ug : https://www.registry.co.ug/ +ug +co.ug +or.ug +ac.ug +sc.ug +go.ug +ne.ug +com.ug +org.ug + +// uk : https://en.wikipedia.org/wiki/.uk +// Submitted by registry +uk +ac.uk +co.uk +gov.uk +ltd.uk +me.uk +net.uk +nhs.uk +org.uk +plc.uk +police.uk +*.sch.uk + +// us : https://en.wikipedia.org/wiki/.us +us +dni.us +fed.us +isa.us +kids.us +nsn.us +// us geographic names +ak.us +al.us +ar.us +as.us +az.us +ca.us +co.us +ct.us +dc.us +de.us +fl.us +ga.us +gu.us +hi.us +ia.us +id.us +il.us +in.us +ks.us +ky.us +la.us +ma.us +md.us +me.us +mi.us +mn.us +mo.us +ms.us +mt.us +nc.us +nd.us +ne.us +nh.us +nj.us +nm.us +nv.us +ny.us +oh.us +ok.us +or.us +pa.us +pr.us +ri.us +sc.us +sd.us +tn.us +tx.us +ut.us +vi.us +vt.us +va.us +wa.us +wi.us +wv.us +wy.us +// The registrar notes several more specific domains available in each state, +// such as state.*.us, dst.*.us, etc., but resolution of these is somewhat +// haphazard; in some states these domains resolve as addresses, while in others +// only subdomains are available, or even nothing at all. We include the +// most common ones where it's clear that different sites are different +// entities. +k12.ak.us +k12.al.us +k12.ar.us +k12.as.us +k12.az.us +k12.ca.us +k12.co.us +k12.ct.us +k12.dc.us +k12.de.us +k12.fl.us +k12.ga.us +k12.gu.us +// k12.hi.us Bug 614565 - Hawaii has a state-wide DOE login +k12.ia.us +k12.id.us +k12.il.us +k12.in.us +k12.ks.us +k12.ky.us +k12.la.us +k12.ma.us +k12.md.us +k12.me.us +k12.mi.us +k12.mn.us +k12.mo.us +k12.ms.us +k12.mt.us +k12.nc.us +// k12.nd.us Bug 1028347 - Removed at request of Travis Rosso +k12.ne.us +k12.nh.us +k12.nj.us +k12.nm.us +k12.nv.us +k12.ny.us +k12.oh.us +k12.ok.us +k12.or.us +k12.pa.us +k12.pr.us +k12.ri.us +k12.sc.us +// k12.sd.us Bug 934131 - Removed at request of James Booze +k12.tn.us +k12.tx.us +k12.ut.us +k12.vi.us +k12.vt.us +k12.va.us +k12.wa.us +k12.wi.us +// k12.wv.us Bug 947705 - Removed at request of Verne Britton +k12.wy.us +cc.ak.us +cc.al.us +cc.ar.us +cc.as.us +cc.az.us +cc.ca.us +cc.co.us +cc.ct.us +cc.dc.us +cc.de.us +cc.fl.us +cc.ga.us +cc.gu.us +cc.hi.us +cc.ia.us +cc.id.us +cc.il.us +cc.in.us +cc.ks.us +cc.ky.us +cc.la.us +cc.ma.us +cc.md.us +cc.me.us +cc.mi.us +cc.mn.us +cc.mo.us +cc.ms.us +cc.mt.us +cc.nc.us +cc.nd.us +cc.ne.us +cc.nh.us +cc.nj.us +cc.nm.us +cc.nv.us +cc.ny.us +cc.oh.us +cc.ok.us +cc.or.us +cc.pa.us +cc.pr.us +cc.ri.us +cc.sc.us +cc.sd.us +cc.tn.us +cc.tx.us +cc.ut.us +cc.vi.us +cc.vt.us +cc.va.us +cc.wa.us +cc.wi.us +cc.wv.us +cc.wy.us +lib.ak.us +lib.al.us +lib.ar.us +lib.as.us +lib.az.us +lib.ca.us +lib.co.us +lib.ct.us +lib.dc.us +// lib.de.us Issue #243 - Moved to Private section at request of Ed Moore +lib.fl.us +lib.ga.us +lib.gu.us +lib.hi.us +lib.ia.us +lib.id.us +lib.il.us +lib.in.us +lib.ks.us +lib.ky.us +lib.la.us +lib.ma.us +lib.md.us +lib.me.us +lib.mi.us +lib.mn.us +lib.mo.us +lib.ms.us +lib.mt.us +lib.nc.us +lib.nd.us +lib.ne.us +lib.nh.us +lib.nj.us +lib.nm.us +lib.nv.us +lib.ny.us +lib.oh.us +lib.ok.us +lib.or.us +lib.pa.us +lib.pr.us +lib.ri.us +lib.sc.us +lib.sd.us +lib.tn.us +lib.tx.us +lib.ut.us +lib.vi.us +lib.vt.us +lib.va.us +lib.wa.us +lib.wi.us +// lib.wv.us Bug 941670 - Removed at request of Larry W Arnold +lib.wy.us +// k12.ma.us contains school districts in Massachusetts. The 4LDs are +// managed independently except for private (PVT), charter (CHTR) and +// parochial (PAROCH) schools. Those are delegated directly to the +// 5LD operators. +pvt.k12.ma.us +chtr.k12.ma.us +paroch.k12.ma.us +// Merit Network, Inc. maintains the registry for =~ /(k12|cc|lib).mi.us/ and the following +// see also: http://domreg.merit.edu +// see also: whois -h whois.domreg.merit.edu help +ann-arbor.mi.us +cog.mi.us +dst.mi.us +eaton.mi.us +gen.mi.us +mus.mi.us +tec.mi.us +washtenaw.mi.us + +// uy : http://www.nic.org.uy/ +uy +com.uy +edu.uy +gub.uy +mil.uy +net.uy +org.uy + +// uz : http://www.reg.uz/ +uz +co.uz +com.uz +net.uz +org.uz + +// va : https://en.wikipedia.org/wiki/.va +va + +// vc : https://en.wikipedia.org/wiki/.vc +// Submitted by registry +vc +com.vc +net.vc +org.vc +gov.vc +mil.vc +edu.vc + +// ve : https://registro.nic.ve/ +// Submitted by registry +ve +arts.ve +co.ve +com.ve +e12.ve +edu.ve +firm.ve +gob.ve +gov.ve +info.ve +int.ve +mil.ve +net.ve +org.ve +rec.ve +store.ve +tec.ve +web.ve + +// vg : https://en.wikipedia.org/wiki/.vg +vg + +// vi : http://www.nic.vi/newdomainform.htm +// http://www.nic.vi/Domain_Rules/body_domain_rules.html indicates some other +// TLDs are "reserved", such as edu.vi and gov.vi, but doesn't actually say they +// are available for registration (which they do not seem to be). +vi +co.vi +com.vi +k12.vi +net.vi +org.vi + +// vn : https://www.dot.vn/vnnic/vnnic/domainregistration.jsp +vn +com.vn +net.vn +org.vn +edu.vn +gov.vn +int.vn +ac.vn +biz.vn +info.vn +name.vn +pro.vn +health.vn + +// vu : https://en.wikipedia.org/wiki/.vu +// http://www.vunic.vu/ +vu +com.vu +edu.vu +net.vu +org.vu + +// wf : http://www.afnic.fr/medias/documents/AFNIC-naming-policy2012.pdf +wf + +// ws : https://en.wikipedia.org/wiki/.ws +// http://samoanic.ws/index.dhtml +ws +com.ws +net.ws +org.ws +gov.ws +edu.ws + +// yt : http://www.afnic.fr/medias/documents/AFNIC-naming-policy2012.pdf +yt + +// IDN ccTLDs +// When submitting patches, please maintain a sort by ISO 3166 ccTLD, then +// U-label, and follow this format: +// // A-Label ("", [, variant info]) : +// // [sponsoring org] +// U-Label + +// xn--mgbaam7a8h ("Emerat", Arabic) : AE +// http://nic.ae/english/arabicdomain/rules.jsp +امارات + +// xn--y9a3aq ("hye", Armenian) : AM +// ISOC AM (operated by .am Registry) +հայ + +// xn--54b7fta0cc ("Bangla", Bangla) : BD +বাংলা + +// xn--90ae ("bg", Bulgarian) : BG +бг + +// xn--90ais ("bel", Belarusian/Russian Cyrillic) : BY +// Operated by .by registry +бел + +// xn--fiqs8s ("Zhongguo/China", Chinese, Simplified) : CN +// CNNIC +// http://cnnic.cn/html/Dir/2005/10/11/3218.htm +中国 + +// xn--fiqz9s ("Zhongguo/China", Chinese, Traditional) : CN +// CNNIC +// http://cnnic.cn/html/Dir/2005/10/11/3218.htm +中國 + +// xn--lgbbat1ad8j ("Algeria/Al Jazair", Arabic) : DZ +الجزائر + +// xn--wgbh1c ("Egypt/Masr", Arabic) : EG +// http://www.dotmasr.eg/ +مصر + +// xn--e1a4c ("eu", Cyrillic) : EU +ею + +// xn--node ("ge", Georgian Mkhedruli) : GE +გე + +// xn--qxam ("el", Greek) : GR +// Hellenic Ministry of Infrastructure, Transport, and Networks +ελ + +// xn--j6w193g ("Hong Kong", Chinese) : HK +// https://www.hkirc.hk +// Submitted by registry +// https://www.hkirc.hk/content.jsp?id=30#!/34 +香港 +公司.香港 +教育.香港 +政府.香港 +個人.香港 +網絡.香港 +組織.香港 + +// xn--2scrj9c ("Bharat", Kannada) : IN +// India +ಭಾರತ + +// xn--3hcrj9c ("Bharat", Oriya) : IN +// India +ଭାରତ + +// xn--45br5cyl ("Bharatam", Assamese) : IN +// India +ভাৰত + +// xn--h2breg3eve ("Bharatam", Sanskrit) : IN +// India +भारतम् + +// xn--h2brj9c8c ("Bharot", Santali) : IN +// India +भारोत + +// xn--mgbgu82a ("Bharat", Sindhi) : IN +// India +ڀارت + +// xn--rvc1e0am3e ("Bharatam", Malayalam) : IN +// India +ഭാരതം + +// xn--h2brj9c ("Bharat", Devanagari) : IN +// India +भारत + +// xn--mgbbh1a ("Bharat", Kashmiri) : IN +// India +بارت + +// xn--mgbbh1a71e ("Bharat", Arabic) : IN +// India +بھارت + +// xn--fpcrj9c3d ("Bharat", Telugu) : IN +// India +భారత్ + +// xn--gecrj9c ("Bharat", Gujarati) : IN +// India +ભારત + +// xn--s9brj9c ("Bharat", Gurmukhi) : IN +// India +ਭਾਰਤ + +// xn--45brj9c ("Bharat", Bengali) : IN +// India +ভারত + +// xn--xkc2dl3a5ee0h ("India", Tamil) : IN +// India +இந்தியா + +// xn--mgba3a4f16a ("Iran", Persian) : IR +ایران + +// xn--mgba3a4fra ("Iran", Arabic) : IR +ايران + +// xn--mgbtx2b ("Iraq", Arabic) : IQ +// Communications and Media Commission +عراق + +// xn--mgbayh7gpa ("al-Ordon", Arabic) : JO +// National Information Technology Center (NITC) +// Royal Scientific Society, Al-Jubeiha +الاردن + +// xn--3e0b707e ("Republic of Korea", Hangul) : KR +한국 + +// xn--80ao21a ("Kaz", Kazakh) : KZ +қаз + +// xn--fzc2c9e2c ("Lanka", Sinhalese-Sinhala) : LK +// http://nic.lk +ලංකා + +// xn--xkc2al3hye2a ("Ilangai", Tamil) : LK +// http://nic.lk +இலங்கை + +// xn--mgbc0a9azcg ("Morocco/al-Maghrib", Arabic) : MA +المغرب + +// xn--d1alf ("mkd", Macedonian) : MK +// MARnet +мкд + +// xn--l1acc ("mon", Mongolian) : MN +мон + +// xn--mix891f ("Macao", Chinese, Traditional) : MO +// MONIC / HNET Asia (Registry Operator for .mo) +澳門 + +// xn--mix082f ("Macao", Chinese, Simplified) : MO +澳门 + +// xn--mgbx4cd0ab ("Malaysia", Malay) : MY +مليسيا + +// xn--mgb9awbf ("Oman", Arabic) : OM +عمان + +// xn--mgbai9azgqp6j ("Pakistan", Urdu/Arabic) : PK +پاکستان + +// xn--mgbai9a5eva00b ("Pakistan", Urdu/Arabic, variant) : PK +پاكستان + +// xn--ygbi2ammx ("Falasteen", Arabic) : PS +// The Palestinian National Internet Naming Authority (PNINA) +// http://www.pnina.ps +فلسطين + +// xn--90a3ac ("srb", Cyrillic) : RS +// https://www.rnids.rs/en/domains/national-domains +срб +пр.срб +орг.срб +обр.срб +од.срб +упр.срб +ак.срб + +// xn--p1ai ("rf", Russian-Cyrillic) : RU +// http://www.cctld.ru/en/docs/rulesrf.php +рф + +// xn--wgbl6a ("Qatar", Arabic) : QA +// http://www.ict.gov.qa/ +قطر + +// xn--mgberp4a5d4ar ("AlSaudiah", Arabic) : SA +// http://www.nic.net.sa/ +السعودية + +// xn--mgberp4a5d4a87g ("AlSaudiah", Arabic, variant) : SA +السعودیة + +// xn--mgbqly7c0a67fbc ("AlSaudiah", Arabic, variant) : SA +السعودیۃ + +// xn--mgbqly7cvafr ("AlSaudiah", Arabic, variant) : SA +السعوديه + +// xn--mgbpl2fh ("sudan", Arabic) : SD +// Operated by .sd registry +سودان + +// xn--yfro4i67o Singapore ("Singapore", Chinese) : SG +新加坡 + +// xn--clchc0ea0b2g2a9gcd ("Singapore", Tamil) : SG +சிங்கப்பூர் + +// xn--ogbpf8fl ("Syria", Arabic) : SY +سورية + +// xn--mgbtf8fl ("Syria", Arabic, variant) : SY +سوريا + +// xn--o3cw4h ("Thai", Thai) : TH +// http://www.thnic.co.th +ไทย +ศึกษา.ไทย +ธุรกิจ.ไทย +รัฐบาล.ไทย +ทหาร.ไทย +เน็ต.ไทย +องค์กร.ไทย + +// xn--pgbs0dh ("Tunisia", Arabic) : TN +// http://nic.tn +تونس + +// xn--kpry57d ("Taiwan", Chinese, Traditional) : TW +// http://www.twnic.net/english/dn/dn_07a.htm +台灣 + +// xn--kprw13d ("Taiwan", Chinese, Simplified) : TW +// http://www.twnic.net/english/dn/dn_07a.htm +台湾 + +// xn--nnx388a ("Taiwan", Chinese, variant) : TW +臺灣 + +// xn--j1amh ("ukr", Cyrillic) : UA +укр + +// xn--mgb2ddes ("AlYemen", Arabic) : YE +اليمن + +// xxx : http://icmregistry.com +xxx + +// ye : http://www.y.net.ye/services/domain_name.htm +*.ye + +// za : http://www.zadna.org.za/content/page/domain-information +ac.za +agric.za +alt.za +co.za +edu.za +gov.za +grondar.za +law.za +mil.za +net.za +ngo.za +nis.za +nom.za +org.za +school.za +tm.za +web.za + +// zm : https://zicta.zm/ +// Submitted by registry +zm +ac.zm +biz.zm +co.zm +com.zm +edu.zm +gov.zm +info.zm +mil.zm +net.zm +org.zm +sch.zm + +// zw : https://www.potraz.gov.zw/ +// Confirmed by registry 2017-01-25 +zw +ac.zw +co.zw +gov.zw +mil.zw +org.zw + + +// newGTLDs +// List of new gTLDs imported from https://newgtlds.icann.org/newgtlds.csv on 2018-05-08T19:40:37Z +// This list is auto-generated, don't edit it manually. + +// aaa : 2015-02-26 American Automobile Association, Inc. +aaa + +// aarp : 2015-05-21 AARP +aarp + +// abarth : 2015-07-30 Fiat Chrysler Automobiles N.V. +abarth + +// abb : 2014-10-24 ABB Ltd +abb + +// abbott : 2014-07-24 Abbott Laboratories, Inc. +abbott + +// abbvie : 2015-07-30 AbbVie Inc. +abbvie + +// abc : 2015-07-30 Disney Enterprises, Inc. +abc + +// able : 2015-06-25 Able Inc. +able + +// abogado : 2014-04-24 Minds + Machines Group Limited +abogado + +// abudhabi : 2015-07-30 Abu Dhabi Systems and Information Centre +abudhabi + +// academy : 2013-11-07 Binky Moon, LLC +academy + +// accenture : 2014-08-15 Accenture plc +accenture + +// accountant : 2014-11-20 dot Accountant Limited +accountant + +// accountants : 2014-03-20 Binky Moon, LLC +accountants + +// aco : 2015-01-08 ACO Severin Ahlmann GmbH & Co. KG +aco + +// active : 2014-05-01 Active Network, LLC +active + +// actor : 2013-12-12 United TLD Holdco Ltd. +actor + +// adac : 2015-07-16 Allgemeiner Deutscher Automobil-Club e.V. (ADAC) +adac + +// ads : 2014-12-04 Charleston Road Registry Inc. +ads + +// adult : 2014-10-16 ICM Registry AD LLC +adult + +// aeg : 2015-03-19 Aktiebolaget Electrolux +aeg + +// aetna : 2015-05-21 Aetna Life Insurance Company +aetna + +// afamilycompany : 2015-07-23 Johnson Shareholdings, Inc. +afamilycompany + +// afl : 2014-10-02 Australian Football League +afl + +// africa : 2014-03-24 ZA Central Registry NPC trading as Registry.Africa +africa + +// agakhan : 2015-04-23 Fondation Aga Khan (Aga Khan Foundation) +agakhan + +// agency : 2013-11-14 Binky Moon, LLC +agency + +// aig : 2014-12-18 American International Group, Inc. +aig + +// aigo : 2015-08-06 aigo Digital Technology Co,Ltd. +aigo + +// airbus : 2015-07-30 Airbus S.A.S. +airbus + +// airforce : 2014-03-06 United TLD Holdco Ltd. +airforce + +// airtel : 2014-10-24 Bharti Airtel Limited +airtel + +// akdn : 2015-04-23 Fondation Aga Khan (Aga Khan Foundation) +akdn + +// alfaromeo : 2015-07-31 Fiat Chrysler Automobiles N.V. +alfaromeo + +// alibaba : 2015-01-15 Alibaba Group Holding Limited +alibaba + +// alipay : 2015-01-15 Alibaba Group Holding Limited +alipay + +// allfinanz : 2014-07-03 Allfinanz Deutsche Vermögensberatung Aktiengesellschaft +allfinanz + +// allstate : 2015-07-31 Allstate Fire and Casualty Insurance Company +allstate + +// ally : 2015-06-18 Ally Financial Inc. +ally + +// alsace : 2014-07-02 Region Grand Est +alsace + +// alstom : 2015-07-30 ALSTOM +alstom + +// americanexpress : 2015-07-31 American Express Travel Related Services Company, Inc. +americanexpress + +// americanfamily : 2015-07-23 AmFam, Inc. +americanfamily + +// amex : 2015-07-31 American Express Travel Related Services Company, Inc. +amex + +// amfam : 2015-07-23 AmFam, Inc. +amfam + +// amica : 2015-05-28 Amica Mutual Insurance Company +amica + +// amsterdam : 2014-07-24 Gemeente Amsterdam +amsterdam + +// analytics : 2014-12-18 Campus IP LLC +analytics + +// android : 2014-08-07 Charleston Road Registry Inc. +android + +// anquan : 2015-01-08 QIHOO 360 TECHNOLOGY CO. LTD. +anquan + +// anz : 2015-07-31 Australia and New Zealand Banking Group Limited +anz + +// aol : 2015-09-17 Oath Inc. +aol + +// apartments : 2014-12-11 Binky Moon, LLC +apartments + +// app : 2015-05-14 Charleston Road Registry Inc. +app + +// apple : 2015-05-14 Apple Inc. +apple + +// aquarelle : 2014-07-24 Aquarelle.com +aquarelle + +// arab : 2015-11-12 League of Arab States +arab + +// aramco : 2014-11-20 Aramco Services Company +aramco + +// archi : 2014-02-06 Afilias plc +archi + +// army : 2014-03-06 United TLD Holdco Ltd. +army + +// art : 2016-03-24 UK Creative Ideas Limited +art + +// arte : 2014-12-11 Association Relative à la Télévision Européenne G.E.I.E. +arte + +// asda : 2015-07-31 Wal-Mart Stores, Inc. +asda + +// associates : 2014-03-06 Binky Moon, LLC +associates + +// athleta : 2015-07-30 The Gap, Inc. +athleta + +// attorney : 2014-03-20 United TLD Holdco Ltd. +attorney + +// auction : 2014-03-20 United TLD Holdco Ltd. +auction + +// audi : 2015-05-21 AUDI Aktiengesellschaft +audi + +// audible : 2015-06-25 Amazon Registry Services, Inc. +audible + +// audio : 2014-03-20 Uniregistry, Corp. +audio + +// auspost : 2015-08-13 Australian Postal Corporation +auspost + +// author : 2014-12-18 Amazon Registry Services, Inc. +author + +// auto : 2014-11-13 Cars Registry Limited +auto + +// autos : 2014-01-09 DERAutos, LLC +autos + +// avianca : 2015-01-08 Aerovias del Continente Americano S.A. Avianca +avianca + +// aws : 2015-06-25 Amazon Registry Services, Inc. +aws + +// axa : 2013-12-19 AXA SA +axa + +// azure : 2014-12-18 Microsoft Corporation +azure + +// baby : 2015-04-09 Johnson & Johnson Services, Inc. +baby + +// baidu : 2015-01-08 Baidu, Inc. +baidu + +// banamex : 2015-07-30 Citigroup Inc. +banamex + +// bananarepublic : 2015-07-31 The Gap, Inc. +bananarepublic + +// band : 2014-06-12 United TLD Holdco Ltd. +band + +// bank : 2014-09-25 fTLD Registry Services LLC +bank + +// bar : 2013-12-12 Punto 2012 Sociedad Anonima Promotora de Inversion de Capital Variable +bar + +// barcelona : 2014-07-24 Municipi de Barcelona +barcelona + +// barclaycard : 2014-11-20 Barclays Bank PLC +barclaycard + +// barclays : 2014-11-20 Barclays Bank PLC +barclays + +// barefoot : 2015-06-11 Gallo Vineyards, Inc. +barefoot + +// bargains : 2013-11-14 Binky Moon, LLC +bargains + +// baseball : 2015-10-29 MLB Advanced Media DH, LLC +baseball + +// basketball : 2015-08-20 Fédération Internationale de Basketball (FIBA) +basketball + +// bauhaus : 2014-04-17 Werkhaus GmbH +bauhaus + +// bayern : 2014-01-23 Bayern Connect GmbH +bayern + +// bbc : 2014-12-18 British Broadcasting Corporation +bbc + +// bbt : 2015-07-23 BB&T Corporation +bbt + +// bbva : 2014-10-02 BANCO BILBAO VIZCAYA ARGENTARIA, S.A. +bbva + +// bcg : 2015-04-02 The Boston Consulting Group, Inc. +bcg + +// bcn : 2014-07-24 Municipi de Barcelona +bcn + +// beats : 2015-05-14 Beats Electronics, LLC +beats + +// beauty : 2015-12-03 L'Oréal +beauty + +// beer : 2014-01-09 Minds + Machines Group Limited +beer + +// bentley : 2014-12-18 Bentley Motors Limited +bentley + +// berlin : 2013-10-31 dotBERLIN GmbH & Co. KG +berlin + +// best : 2013-12-19 BestTLD Pty Ltd +best + +// bestbuy : 2015-07-31 BBY Solutions, Inc. +bestbuy + +// bet : 2015-05-07 Afilias plc +bet + +// bharti : 2014-01-09 Bharti Enterprises (Holding) Private Limited +bharti + +// bible : 2014-06-19 American Bible Society +bible + +// bid : 2013-12-19 dot Bid Limited +bid + +// bike : 2013-08-27 Binky Moon, LLC +bike + +// bing : 2014-12-18 Microsoft Corporation +bing + +// bingo : 2014-12-04 Binky Moon, LLC +bingo + +// bio : 2014-03-06 Afilias plc +bio + +// black : 2014-01-16 Afilias plc +black + +// blackfriday : 2014-01-16 Uniregistry, Corp. +blackfriday + +// blanco : 2015-07-16 BLANCO GmbH + Co KG +blanco + +// blockbuster : 2015-07-30 Dish DBS Corporation +blockbuster + +// blog : 2015-05-14 Knock Knock WHOIS There, LLC +blog + +// bloomberg : 2014-07-17 Bloomberg IP Holdings LLC +bloomberg + +// blue : 2013-11-07 Afilias plc +blue + +// bms : 2014-10-30 Bristol-Myers Squibb Company +bms + +// bmw : 2014-01-09 Bayerische Motoren Werke Aktiengesellschaft +bmw + +// bnl : 2014-07-24 Banca Nazionale del Lavoro +bnl + +// bnpparibas : 2014-05-29 BNP Paribas +bnpparibas + +// boats : 2014-12-04 DERBoats, LLC +boats + +// boehringer : 2015-07-09 Boehringer Ingelheim International GmbH +boehringer + +// bofa : 2015-07-31 Bank of America Corporation +bofa + +// bom : 2014-10-16 Núcleo de Informação e Coordenação do Ponto BR - NIC.br +bom + +// bond : 2014-06-05 Bond University Limited +bond + +// boo : 2014-01-30 Charleston Road Registry Inc. +boo + +// book : 2015-08-27 Amazon Registry Services, Inc. +book + +// booking : 2015-07-16 Booking.com B.V. +booking + +// bosch : 2015-06-18 Robert Bosch GMBH +bosch + +// bostik : 2015-05-28 Bostik SA +bostik + +// boston : 2015-12-10 Boston TLD Management, LLC +boston + +// bot : 2014-12-18 Amazon Registry Services, Inc. +bot + +// boutique : 2013-11-14 Binky Moon, LLC +boutique + +// box : 2015-11-12 NS1 Limited +box + +// bradesco : 2014-12-18 Banco Bradesco S.A. +bradesco + +// bridgestone : 2014-12-18 Bridgestone Corporation +bridgestone + +// broadway : 2014-12-22 Celebrate Broadway, Inc. +broadway + +// broker : 2014-12-11 Dotbroker Registry Limited +broker + +// brother : 2015-01-29 Brother Industries, Ltd. +brother + +// brussels : 2014-02-06 DNS.be vzw +brussels + +// budapest : 2013-11-21 Minds + Machines Group Limited +budapest + +// bugatti : 2015-07-23 Bugatti International SA +bugatti + +// build : 2013-11-07 Plan Bee LLC +build + +// builders : 2013-11-07 Binky Moon, LLC +builders + +// business : 2013-11-07 Binky Moon, LLC +business + +// buy : 2014-12-18 Amazon Registry Services, Inc. +buy + +// buzz : 2013-10-02 DOTSTRATEGY CO. +buzz + +// bzh : 2014-02-27 Association www.bzh +bzh + +// cab : 2013-10-24 Binky Moon, LLC +cab + +// cafe : 2015-02-11 Binky Moon, LLC +cafe + +// cal : 2014-07-24 Charleston Road Registry Inc. +cal + +// call : 2014-12-18 Amazon Registry Services, Inc. +call + +// calvinklein : 2015-07-30 PVH gTLD Holdings LLC +calvinklein + +// cam : 2016-04-21 AC Webconnecting Holding B.V. +cam + +// camera : 2013-08-27 Binky Moon, LLC +camera + +// camp : 2013-11-07 Binky Moon, LLC +camp + +// cancerresearch : 2014-05-15 Australian Cancer Research Foundation +cancerresearch + +// canon : 2014-09-12 Canon Inc. +canon + +// capetown : 2014-03-24 ZA Central Registry NPC trading as ZA Central Registry +capetown + +// capital : 2014-03-06 Binky Moon, LLC +capital + +// capitalone : 2015-08-06 Capital One Financial Corporation +capitalone + +// car : 2015-01-22 Cars Registry Limited +car + +// caravan : 2013-12-12 Caravan International, Inc. +caravan + +// cards : 2013-12-05 Binky Moon, LLC +cards + +// care : 2014-03-06 Binky Moon, LLC +care + +// career : 2013-10-09 dotCareer LLC +career + +// careers : 2013-10-02 Binky Moon, LLC +careers + +// cars : 2014-11-13 Cars Registry Limited +cars + +// cartier : 2014-06-23 Richemont DNS Inc. +cartier + +// casa : 2013-11-21 Minds + Machines Group Limited +casa + +// case : 2015-09-03 CNH Industrial N.V. +case + +// caseih : 2015-09-03 CNH Industrial N.V. +caseih + +// cash : 2014-03-06 Binky Moon, LLC +cash + +// casino : 2014-12-18 Binky Moon, LLC +casino + +// catering : 2013-12-05 Binky Moon, LLC +catering + +// catholic : 2015-10-21 Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) +catholic + +// cba : 2014-06-26 COMMONWEALTH BANK OF AUSTRALIA +cba + +// cbn : 2014-08-22 The Christian Broadcasting Network, Inc. +cbn + +// cbre : 2015-07-02 CBRE, Inc. +cbre + +// cbs : 2015-08-06 CBS Domains Inc. +cbs + +// ceb : 2015-04-09 The Corporate Executive Board Company +ceb + +// center : 2013-11-07 Binky Moon, LLC +center + +// ceo : 2013-11-07 CEOTLD Pty Ltd +ceo + +// cern : 2014-06-05 European Organization for Nuclear Research ("CERN") +cern + +// cfa : 2014-08-28 CFA Institute +cfa + +// cfd : 2014-12-11 DotCFD Registry Limited +cfd + +// chanel : 2015-04-09 Chanel International B.V. +chanel + +// channel : 2014-05-08 Charleston Road Registry Inc. +channel + +// charity : 2018-04-11 Corn Lake, LLC +charity + +// chase : 2015-04-30 JPMorgan Chase Bank, National Association +chase + +// chat : 2014-12-04 Binky Moon, LLC +chat + +// cheap : 2013-11-14 Binky Moon, LLC +cheap + +// chintai : 2015-06-11 CHINTAI Corporation +chintai + +// christmas : 2013-11-21 Uniregistry, Corp. +christmas + +// chrome : 2014-07-24 Charleston Road Registry Inc. +chrome + +// chrysler : 2015-07-30 FCA US LLC. +chrysler + +// church : 2014-02-06 Binky Moon, LLC +church + +// cipriani : 2015-02-19 Hotel Cipriani Srl +cipriani + +// circle : 2014-12-18 Amazon Registry Services, Inc. +circle + +// cisco : 2014-12-22 Cisco Technology, Inc. +cisco + +// citadel : 2015-07-23 Citadel Domain LLC +citadel + +// citi : 2015-07-30 Citigroup Inc. +citi + +// citic : 2014-01-09 CITIC Group Corporation +citic + +// city : 2014-05-29 Binky Moon, LLC +city + +// cityeats : 2014-12-11 Lifestyle Domain Holdings, Inc. +cityeats + +// claims : 2014-03-20 Binky Moon, LLC +claims + +// cleaning : 2013-12-05 Binky Moon, LLC +cleaning + +// click : 2014-06-05 Uniregistry, Corp. +click + +// clinic : 2014-03-20 Binky Moon, LLC +clinic + +// clinique : 2015-10-01 The Estée Lauder Companies Inc. +clinique + +// clothing : 2013-08-27 Binky Moon, LLC +clothing + +// cloud : 2015-04-16 Aruba PEC S.p.A. +cloud + +// club : 2013-11-08 .CLUB DOMAINS, LLC +club + +// clubmed : 2015-06-25 Club Méditerranée S.A. +clubmed + +// coach : 2014-10-09 Binky Moon, LLC +coach + +// codes : 2013-10-31 Binky Moon, LLC +codes + +// coffee : 2013-10-17 Binky Moon, LLC +coffee + +// college : 2014-01-16 XYZ.COM LLC +college + +// cologne : 2014-02-05 punkt.wien GmbH +cologne + +// comcast : 2015-07-23 Comcast IP Holdings I, LLC +comcast + +// commbank : 2014-06-26 COMMONWEALTH BANK OF AUSTRALIA +commbank + +// community : 2013-12-05 Binky Moon, LLC +community + +// company : 2013-11-07 Binky Moon, LLC +company + +// compare : 2015-10-08 iSelect Ltd +compare + +// computer : 2013-10-24 Binky Moon, LLC +computer + +// comsec : 2015-01-08 VeriSign, Inc. +comsec + +// condos : 2013-12-05 Binky Moon, LLC +condos + +// construction : 2013-09-16 Binky Moon, LLC +construction + +// consulting : 2013-12-05 United TLD Holdco Ltd. +consulting + +// contact : 2015-01-08 Top Level Spectrum, Inc. +contact + +// contractors : 2013-09-10 Binky Moon, LLC +contractors + +// cooking : 2013-11-21 Minds + Machines Group Limited +cooking + +// cookingchannel : 2015-07-02 Lifestyle Domain Holdings, Inc. +cookingchannel + +// cool : 2013-11-14 Binky Moon, LLC +cool + +// corsica : 2014-09-25 Collectivité de Corse +corsica + +// country : 2013-12-19 DotCountry LLC +country + +// coupon : 2015-02-26 Amazon Registry Services, Inc. +coupon + +// coupons : 2015-03-26 Binky Moon, LLC +coupons + +// courses : 2014-12-04 OPEN UNIVERSITIES AUSTRALIA PTY LTD +courses + +// credit : 2014-03-20 Binky Moon, LLC +credit + +// creditcard : 2014-03-20 Binky Moon, LLC +creditcard + +// creditunion : 2015-01-22 CUNA Performance Resources, LLC +creditunion + +// cricket : 2014-10-09 dot Cricket Limited +cricket + +// crown : 2014-10-24 Crown Equipment Corporation +crown + +// crs : 2014-04-03 Federated Co-operatives Limited +crs + +// cruise : 2015-12-10 Viking River Cruises (Bermuda) Ltd. +cruise + +// cruises : 2013-12-05 Binky Moon, LLC +cruises + +// csc : 2014-09-25 Alliance-One Services, Inc. +csc + +// cuisinella : 2014-04-03 SALM S.A.S. +cuisinella + +// cymru : 2014-05-08 Nominet UK +cymru + +// cyou : 2015-01-22 Beijing Gamease Age Digital Technology Co., Ltd. +cyou + +// dabur : 2014-02-06 Dabur India Limited +dabur + +// dad : 2014-01-23 Charleston Road Registry Inc. +dad + +// dance : 2013-10-24 United TLD Holdco Ltd. +dance + +// data : 2016-06-02 Dish DBS Corporation +data + +// date : 2014-11-20 dot Date Limited +date + +// dating : 2013-12-05 Binky Moon, LLC +dating + +// datsun : 2014-03-27 NISSAN MOTOR CO., LTD. +datsun + +// day : 2014-01-30 Charleston Road Registry Inc. +day + +// dclk : 2014-11-20 Charleston Road Registry Inc. +dclk + +// dds : 2015-05-07 Minds + Machines Group Limited +dds + +// deal : 2015-06-25 Amazon Registry Services, Inc. +deal + +// dealer : 2014-12-22 Dealer Dot Com, Inc. +dealer + +// deals : 2014-05-22 Binky Moon, LLC +deals + +// degree : 2014-03-06 United TLD Holdco Ltd. +degree + +// delivery : 2014-09-11 Binky Moon, LLC +delivery + +// dell : 2014-10-24 Dell Inc. +dell + +// deloitte : 2015-07-31 Deloitte Touche Tohmatsu +deloitte + +// delta : 2015-02-19 Delta Air Lines, Inc. +delta + +// democrat : 2013-10-24 United TLD Holdco Ltd. +democrat + +// dental : 2014-03-20 Binky Moon, LLC +dental + +// dentist : 2014-03-20 United TLD Holdco Ltd. +dentist + +// desi : 2013-11-14 Desi Networks LLC +desi + +// design : 2014-11-07 Top Level Design, LLC +design + +// dev : 2014-10-16 Charleston Road Registry Inc. +dev + +// dhl : 2015-07-23 Deutsche Post AG +dhl + +// diamonds : 2013-09-22 Binky Moon, LLC +diamonds + +// diet : 2014-06-26 Uniregistry, Corp. +diet + +// digital : 2014-03-06 Binky Moon, LLC +digital + +// direct : 2014-04-10 Binky Moon, LLC +direct + +// directory : 2013-09-20 Binky Moon, LLC +directory + +// discount : 2014-03-06 Binky Moon, LLC +discount + +// discover : 2015-07-23 Discover Financial Services +discover + +// dish : 2015-07-30 Dish DBS Corporation +dish + +// diy : 2015-11-05 Lifestyle Domain Holdings, Inc. +diy + +// dnp : 2013-12-13 Dai Nippon Printing Co., Ltd. +dnp + +// docs : 2014-10-16 Charleston Road Registry Inc. +docs + +// doctor : 2016-06-02 Binky Moon, LLC +doctor + +// dodge : 2015-07-30 FCA US LLC. +dodge + +// dog : 2014-12-04 Binky Moon, LLC +dog + +// doha : 2014-09-18 Communications Regulatory Authority (CRA) +doha + +// domains : 2013-10-17 Binky Moon, LLC +domains + +// dot : 2015-05-21 Dish DBS Corporation +dot + +// download : 2014-11-20 dot Support Limited +download + +// drive : 2015-03-05 Charleston Road Registry Inc. +drive + +// dtv : 2015-06-04 Dish DBS Corporation +dtv + +// dubai : 2015-01-01 Dubai Smart Government Department +dubai + +// duck : 2015-07-23 Johnson Shareholdings, Inc. +duck + +// dunlop : 2015-07-02 The Goodyear Tire & Rubber Company +dunlop + +// duns : 2015-08-06 The Dun & Bradstreet Corporation +duns + +// dupont : 2015-06-25 E. I. du Pont de Nemours and Company +dupont + +// durban : 2014-03-24 ZA Central Registry NPC trading as ZA Central Registry +durban + +// dvag : 2014-06-23 Deutsche Vermögensberatung Aktiengesellschaft DVAG +dvag + +// dvr : 2016-05-26 Hughes Satellite Systems Corporation +dvr + +// earth : 2014-12-04 Interlink Co., Ltd. +earth + +// eat : 2014-01-23 Charleston Road Registry Inc. +eat + +// eco : 2016-07-08 Big Room Inc. +eco + +// edeka : 2014-12-18 EDEKA Verband kaufmännischer Genossenschaften e.V. +edeka + +// education : 2013-11-07 Binky Moon, LLC +education + +// email : 2013-10-31 Binky Moon, LLC +email + +// emerck : 2014-04-03 Merck KGaA +emerck + +// energy : 2014-09-11 Binky Moon, LLC +energy + +// engineer : 2014-03-06 United TLD Holdco Ltd. +engineer + +// engineering : 2014-03-06 Binky Moon, LLC +engineering + +// enterprises : 2013-09-20 Binky Moon, LLC +enterprises + +// epost : 2015-07-23 Deutsche Post AG +epost + +// epson : 2014-12-04 Seiko Epson Corporation +epson + +// equipment : 2013-08-27 Binky Moon, LLC +equipment + +// ericsson : 2015-07-09 Telefonaktiebolaget L M Ericsson +ericsson + +// erni : 2014-04-03 ERNI Group Holding AG +erni + +// esq : 2014-05-08 Charleston Road Registry Inc. +esq + +// estate : 2013-08-27 Binky Moon, LLC +estate + +// esurance : 2015-07-23 Esurance Insurance Company +esurance + +// etisalat : 2015-09-03 Emirates Telecommunications Corporation (trading as Etisalat) +etisalat + +// eurovision : 2014-04-24 European Broadcasting Union (EBU) +eurovision + +// eus : 2013-12-12 Puntueus Fundazioa +eus + +// events : 2013-12-05 Binky Moon, LLC +events + +// everbank : 2014-05-15 EverBank +everbank + +// exchange : 2014-03-06 Binky Moon, LLC +exchange + +// expert : 2013-11-21 Binky Moon, LLC +expert + +// exposed : 2013-12-05 Binky Moon, LLC +exposed + +// express : 2015-02-11 Binky Moon, LLC +express + +// extraspace : 2015-05-14 Extra Space Storage LLC +extraspace + +// fage : 2014-12-18 Fage International S.A. +fage + +// fail : 2014-03-06 Binky Moon, LLC +fail + +// fairwinds : 2014-11-13 FairWinds Partners, LLC +fairwinds + +// faith : 2014-11-20 dot Faith Limited +faith + +// family : 2015-04-02 United TLD Holdco Ltd. +family + +// fan : 2014-03-06 Asiamix Digital Limited +fan + +// fans : 2014-11-07 Asiamix Digital Limited +fans + +// farm : 2013-11-07 Binky Moon, LLC +farm + +// farmers : 2015-07-09 Farmers Insurance Exchange +farmers + +// fashion : 2014-07-03 Minds + Machines Group Limited +fashion + +// fast : 2014-12-18 Amazon Registry Services, Inc. +fast + +// fedex : 2015-08-06 Federal Express Corporation +fedex + +// feedback : 2013-12-19 Top Level Spectrum, Inc. +feedback + +// ferrari : 2015-07-31 Fiat Chrysler Automobiles N.V. +ferrari + +// ferrero : 2014-12-18 Ferrero Trading Lux S.A. +ferrero + +// fiat : 2015-07-31 Fiat Chrysler Automobiles N.V. +fiat + +// fidelity : 2015-07-30 Fidelity Brokerage Services LLC +fidelity + +// fido : 2015-08-06 Rogers Communications Canada Inc. +fido + +// film : 2015-01-08 Motion Picture Domain Registry Pty Ltd +film + +// final : 2014-10-16 Núcleo de Informação e Coordenação do Ponto BR - NIC.br +final + +// finance : 2014-03-20 Binky Moon, LLC +finance + +// financial : 2014-03-06 Binky Moon, LLC +financial + +// fire : 2015-06-25 Amazon Registry Services, Inc. +fire + +// firestone : 2014-12-18 Bridgestone Licensing Services, Inc +firestone + +// firmdale : 2014-03-27 Firmdale Holdings Limited +firmdale + +// fish : 2013-12-12 Binky Moon, LLC +fish + +// fishing : 2013-11-21 Minds + Machines Group Limited +fishing + +// fit : 2014-11-07 Minds + Machines Group Limited +fit + +// fitness : 2014-03-06 Binky Moon, LLC +fitness + +// flickr : 2015-04-02 Yahoo! Domain Services Inc. +flickr + +// flights : 2013-12-05 Binky Moon, LLC +flights + +// flir : 2015-07-23 FLIR Systems, Inc. +flir + +// florist : 2013-11-07 Binky Moon, LLC +florist + +// flowers : 2014-10-09 Uniregistry, Corp. +flowers + +// fly : 2014-05-08 Charleston Road Registry Inc. +fly + +// foo : 2014-01-23 Charleston Road Registry Inc. +foo + +// food : 2016-04-21 Lifestyle Domain Holdings, Inc. +food + +// foodnetwork : 2015-07-02 Lifestyle Domain Holdings, Inc. +foodnetwork + +// football : 2014-12-18 Binky Moon, LLC +football + +// ford : 2014-11-13 Ford Motor Company +ford + +// forex : 2014-12-11 Dotforex Registry Limited +forex + +// forsale : 2014-05-22 United TLD Holdco Ltd. +forsale + +// forum : 2015-04-02 Fegistry, LLC +forum + +// foundation : 2013-12-05 Binky Moon, LLC +foundation + +// fox : 2015-09-11 FOX Registry, LLC +fox + +// free : 2015-12-10 Amazon Registry Services, Inc. +free + +// fresenius : 2015-07-30 Fresenius Immobilien-Verwaltungs-GmbH +fresenius + +// frl : 2014-05-15 FRLregistry B.V. +frl + +// frogans : 2013-12-19 OP3FT +frogans + +// frontdoor : 2015-07-02 Lifestyle Domain Holdings, Inc. +frontdoor + +// frontier : 2015-02-05 Frontier Communications Corporation +frontier + +// ftr : 2015-07-16 Frontier Communications Corporation +ftr + +// fujitsu : 2015-07-30 Fujitsu Limited +fujitsu + +// fujixerox : 2015-07-23 Xerox DNHC LLC +fujixerox + +// fun : 2016-01-14 DotSpace Inc. +fun + +// fund : 2014-03-20 Binky Moon, LLC +fund + +// furniture : 2014-03-20 Binky Moon, LLC +furniture + +// futbol : 2013-09-20 United TLD Holdco Ltd. +futbol + +// fyi : 2015-04-02 Binky Moon, LLC +fyi + +// gal : 2013-11-07 Asociación puntoGAL +gal + +// gallery : 2013-09-13 Binky Moon, LLC +gallery + +// gallo : 2015-06-11 Gallo Vineyards, Inc. +gallo + +// gallup : 2015-02-19 Gallup, Inc. +gallup + +// game : 2015-05-28 Uniregistry, Corp. +game + +// games : 2015-05-28 United TLD Holdco Ltd. +games + +// gap : 2015-07-31 The Gap, Inc. +gap + +// garden : 2014-06-26 Minds + Machines Group Limited +garden + +// gbiz : 2014-07-17 Charleston Road Registry Inc. +gbiz + +// gdn : 2014-07-31 Joint Stock Company "Navigation-information systems" +gdn + +// gea : 2014-12-04 GEA Group Aktiengesellschaft +gea + +// gent : 2014-01-23 COMBELL NV +gent + +// genting : 2015-03-12 Resorts World Inc Pte. Ltd. +genting + +// george : 2015-07-31 Wal-Mart Stores, Inc. +george + +// ggee : 2014-01-09 GMO Internet, Inc. +ggee + +// gift : 2013-10-17 DotGift, LLC +gift + +// gifts : 2014-07-03 Binky Moon, LLC +gifts + +// gives : 2014-03-06 United TLD Holdco Ltd. +gives + +// giving : 2014-11-13 Giving Limited +giving + +// glade : 2015-07-23 Johnson Shareholdings, Inc. +glade + +// glass : 2013-11-07 Binky Moon, LLC +glass + +// gle : 2014-07-24 Charleston Road Registry Inc. +gle + +// global : 2014-04-17 Dot Global Domain Registry Limited +global + +// globo : 2013-12-19 Globo Comunicação e Participações S.A +globo + +// gmail : 2014-05-01 Charleston Road Registry Inc. +gmail + +// gmbh : 2016-01-29 Binky Moon, LLC +gmbh + +// gmo : 2014-01-09 GMO Internet Pte. Ltd. +gmo + +// gmx : 2014-04-24 1&1 Mail & Media GmbH +gmx + +// godaddy : 2015-07-23 Go Daddy East, LLC +godaddy + +// gold : 2015-01-22 Binky Moon, LLC +gold + +// goldpoint : 2014-11-20 YODOBASHI CAMERA CO.,LTD. +goldpoint + +// golf : 2014-12-18 Binky Moon, LLC +golf + +// goo : 2014-12-18 NTT Resonant Inc. +goo + +// goodhands : 2015-07-31 Allstate Fire and Casualty Insurance Company +goodhands + +// goodyear : 2015-07-02 The Goodyear Tire & Rubber Company +goodyear + +// goog : 2014-11-20 Charleston Road Registry Inc. +goog + +// google : 2014-07-24 Charleston Road Registry Inc. +google + +// gop : 2014-01-16 Republican State Leadership Committee, Inc. +gop + +// got : 2014-12-18 Amazon Registry Services, Inc. +got + +// grainger : 2015-05-07 Grainger Registry Services, LLC +grainger + +// graphics : 2013-09-13 Binky Moon, LLC +graphics + +// gratis : 2014-03-20 Binky Moon, LLC +gratis + +// green : 2014-05-08 Afilias plc +green + +// gripe : 2014-03-06 Binky Moon, LLC +gripe + +// grocery : 2016-06-16 Wal-Mart Stores, Inc. +grocery + +// group : 2014-08-15 Binky Moon, LLC +group + +// guardian : 2015-07-30 The Guardian Life Insurance Company of America +guardian + +// gucci : 2014-11-13 Guccio Gucci S.p.a. +gucci + +// guge : 2014-08-28 Charleston Road Registry Inc. +guge + +// guide : 2013-09-13 Binky Moon, LLC +guide + +// guitars : 2013-11-14 Uniregistry, Corp. +guitars + +// guru : 2013-08-27 Binky Moon, LLC +guru + +// hair : 2015-12-03 L'Oréal +hair + +// hamburg : 2014-02-20 Hamburg Top-Level-Domain GmbH +hamburg + +// hangout : 2014-11-13 Charleston Road Registry Inc. +hangout + +// haus : 2013-12-05 United TLD Holdco Ltd. +haus + +// hbo : 2015-07-30 HBO Registry Services, Inc. +hbo + +// hdfc : 2015-07-30 HOUSING DEVELOPMENT FINANCE CORPORATION LIMITED +hdfc + +// hdfcbank : 2015-02-12 HDFC Bank Limited +hdfcbank + +// health : 2015-02-11 DotHealth, LLC +health + +// healthcare : 2014-06-12 Binky Moon, LLC +healthcare + +// help : 2014-06-26 Uniregistry, Corp. +help + +// helsinki : 2015-02-05 City of Helsinki +helsinki + +// here : 2014-02-06 Charleston Road Registry Inc. +here + +// hermes : 2014-07-10 HERMES INTERNATIONAL +hermes + +// hgtv : 2015-07-02 Lifestyle Domain Holdings, Inc. +hgtv + +// hiphop : 2014-03-06 Uniregistry, Corp. +hiphop + +// hisamitsu : 2015-07-16 Hisamitsu Pharmaceutical Co.,Inc. +hisamitsu + +// hitachi : 2014-10-31 Hitachi, Ltd. +hitachi + +// hiv : 2014-03-13 Uniregistry, Corp. +hiv + +// hkt : 2015-05-14 PCCW-HKT DataCom Services Limited +hkt + +// hockey : 2015-03-19 Binky Moon, LLC +hockey + +// holdings : 2013-08-27 Binky Moon, LLC +holdings + +// holiday : 2013-11-07 Binky Moon, LLC +holiday + +// homedepot : 2015-04-02 Home Depot Product Authority, LLC +homedepot + +// homegoods : 2015-07-16 The TJX Companies, Inc. +homegoods + +// homes : 2014-01-09 DERHomes, LLC +homes + +// homesense : 2015-07-16 The TJX Companies, Inc. +homesense + +// honda : 2014-12-18 Honda Motor Co., Ltd. +honda + +// honeywell : 2015-07-23 Honeywell GTLD LLC +honeywell + +// horse : 2013-11-21 Minds + Machines Group Limited +horse + +// hospital : 2016-10-20 Binky Moon, LLC +hospital + +// host : 2014-04-17 DotHost Inc. +host + +// hosting : 2014-05-29 Uniregistry, Corp. +hosting + +// hot : 2015-08-27 Amazon Registry Services, Inc. +hot + +// hoteles : 2015-03-05 Travel Reservations SRL +hoteles + +// hotels : 2016-04-07 Booking.com B.V. +hotels + +// hotmail : 2014-12-18 Microsoft Corporation +hotmail + +// house : 2013-11-07 Binky Moon, LLC +house + +// how : 2014-01-23 Charleston Road Registry Inc. +how + +// hsbc : 2014-10-24 HSBC Global Services (UK) Limited +hsbc + +// hughes : 2015-07-30 Hughes Satellite Systems Corporation +hughes + +// hyatt : 2015-07-30 Hyatt GTLD, L.L.C. +hyatt + +// hyundai : 2015-07-09 Hyundai Motor Company +hyundai + +// ibm : 2014-07-31 International Business Machines Corporation +ibm + +// icbc : 2015-02-19 Industrial and Commercial Bank of China Limited +icbc + +// ice : 2014-10-30 IntercontinentalExchange, Inc. +ice + +// icu : 2015-01-08 ShortDot SA +icu + +// ieee : 2015-07-23 IEEE Global LLC +ieee + +// ifm : 2014-01-30 ifm electronic gmbh +ifm + +// ikano : 2015-07-09 Ikano S.A. +ikano + +// imamat : 2015-08-06 Fondation Aga Khan (Aga Khan Foundation) +imamat + +// imdb : 2015-06-25 Amazon Registry Services, Inc. +imdb + +// immo : 2014-07-10 Binky Moon, LLC +immo + +// immobilien : 2013-11-07 United TLD Holdco Ltd. +immobilien + +// inc : 2018-03-10 GTLD Limited +inc + +// industries : 2013-12-05 Binky Moon, LLC +industries + +// infiniti : 2014-03-27 NISSAN MOTOR CO., LTD. +infiniti + +// ing : 2014-01-23 Charleston Road Registry Inc. +ing + +// ink : 2013-12-05 Top Level Design, LLC +ink + +// institute : 2013-11-07 Binky Moon, LLC +institute + +// insurance : 2015-02-19 fTLD Registry Services LLC +insurance + +// insure : 2014-03-20 Binky Moon, LLC +insure + +// intel : 2015-08-06 Intel Corporation +intel + +// international : 2013-11-07 Binky Moon, LLC +international + +// intuit : 2015-07-30 Intuit Administrative Services, Inc. +intuit + +// investments : 2014-03-20 Binky Moon, LLC +investments + +// ipiranga : 2014-08-28 Ipiranga Produtos de Petroleo S.A. +ipiranga + +// irish : 2014-08-07 Binky Moon, LLC +irish + +// iselect : 2015-02-11 iSelect Ltd +iselect + +// ismaili : 2015-08-06 Fondation Aga Khan (Aga Khan Foundation) +ismaili + +// ist : 2014-08-28 Istanbul Metropolitan Municipality +ist + +// istanbul : 2014-08-28 Istanbul Metropolitan Municipality +istanbul + +// itau : 2014-10-02 Itau Unibanco Holding S.A. +itau + +// itv : 2015-07-09 ITV Services Limited +itv + +// iveco : 2015-09-03 CNH Industrial N.V. +iveco + +// jaguar : 2014-11-13 Jaguar Land Rover Ltd +jaguar + +// java : 2014-06-19 Oracle Corporation +java + +// jcb : 2014-11-20 JCB Co., Ltd. +jcb + +// jcp : 2015-04-23 JCP Media, Inc. +jcp + +// jeep : 2015-07-30 FCA US LLC. +jeep + +// jetzt : 2014-01-09 Binky Moon, LLC +jetzt + +// jewelry : 2015-03-05 Binky Moon, LLC +jewelry + +// jio : 2015-04-02 Reliance Industries Limited +jio + +// jlc : 2014-12-04 Richemont DNS Inc. +jlc + +// jll : 2015-04-02 Jones Lang LaSalle Incorporated +jll + +// jmp : 2015-03-26 Matrix IP LLC +jmp + +// jnj : 2015-06-18 Johnson & Johnson Services, Inc. +jnj + +// joburg : 2014-03-24 ZA Central Registry NPC trading as ZA Central Registry +joburg + +// jot : 2014-12-18 Amazon Registry Services, Inc. +jot + +// joy : 2014-12-18 Amazon Registry Services, Inc. +joy + +// jpmorgan : 2015-04-30 JPMorgan Chase Bank, National Association +jpmorgan + +// jprs : 2014-09-18 Japan Registry Services Co., Ltd. +jprs + +// juegos : 2014-03-20 Uniregistry, Corp. +juegos + +// juniper : 2015-07-30 JUNIPER NETWORKS, INC. +juniper + +// kaufen : 2013-11-07 United TLD Holdco Ltd. +kaufen + +// kddi : 2014-09-12 KDDI CORPORATION +kddi + +// kerryhotels : 2015-04-30 Kerry Trading Co. Limited +kerryhotels + +// kerrylogistics : 2015-04-09 Kerry Trading Co. Limited +kerrylogistics + +// kerryproperties : 2015-04-09 Kerry Trading Co. Limited +kerryproperties + +// kfh : 2014-12-04 Kuwait Finance House +kfh + +// kia : 2015-07-09 KIA MOTORS CORPORATION +kia + +// kim : 2013-09-23 Afilias plc +kim + +// kinder : 2014-11-07 Ferrero Trading Lux S.A. +kinder + +// kindle : 2015-06-25 Amazon Registry Services, Inc. +kindle + +// kitchen : 2013-09-20 Binky Moon, LLC +kitchen + +// kiwi : 2013-09-20 DOT KIWI LIMITED +kiwi + +// koeln : 2014-01-09 punkt.wien GmbH +koeln + +// komatsu : 2015-01-08 Komatsu Ltd. +komatsu + +// kosher : 2015-08-20 Kosher Marketing Assets LLC +kosher + +// kpmg : 2015-04-23 KPMG International Cooperative (KPMG International Genossenschaft) +kpmg + +// kpn : 2015-01-08 Koninklijke KPN N.V. +kpn + +// krd : 2013-12-05 KRG Department of Information Technology +krd + +// kred : 2013-12-19 KredTLD Pty Ltd +kred + +// kuokgroup : 2015-04-09 Kerry Trading Co. Limited +kuokgroup + +// kyoto : 2014-11-07 Academic Institution: Kyoto Jyoho Gakuen +kyoto + +// lacaixa : 2014-01-09 Fundación Bancaria Caixa d’Estalvis i Pensions de Barcelona, “la Caixa” +lacaixa + +// ladbrokes : 2015-08-06 LADBROKES INTERNATIONAL PLC +ladbrokes + +// lamborghini : 2015-06-04 Automobili Lamborghini S.p.A. +lamborghini + +// lamer : 2015-10-01 The Estée Lauder Companies Inc. +lamer + +// lancaster : 2015-02-12 LANCASTER +lancaster + +// lancia : 2015-07-31 Fiat Chrysler Automobiles N.V. +lancia + +// lancome : 2015-07-23 L'Oréal +lancome + +// land : 2013-09-10 Binky Moon, LLC +land + +// landrover : 2014-11-13 Jaguar Land Rover Ltd +landrover + +// lanxess : 2015-07-30 LANXESS Corporation +lanxess + +// lasalle : 2015-04-02 Jones Lang LaSalle Incorporated +lasalle + +// lat : 2014-10-16 ECOM-LAC Federaciòn de Latinoamèrica y el Caribe para Internet y el Comercio Electrònico +lat + +// latino : 2015-07-30 Dish DBS Corporation +latino + +// latrobe : 2014-06-16 La Trobe University +latrobe + +// law : 2015-01-22 Minds + Machines Group Limited +law + +// lawyer : 2014-03-20 United TLD Holdco Ltd. +lawyer + +// lds : 2014-03-20 IRI Domain Management, LLC ("Applicant") +lds + +// lease : 2014-03-06 Binky Moon, LLC +lease + +// leclerc : 2014-08-07 A.C.D. LEC Association des Centres Distributeurs Edouard Leclerc +leclerc + +// lefrak : 2015-07-16 LeFrak Organization, Inc. +lefrak + +// legal : 2014-10-16 Binky Moon, LLC +legal + +// lego : 2015-07-16 LEGO Juris A/S +lego + +// lexus : 2015-04-23 TOYOTA MOTOR CORPORATION +lexus + +// lgbt : 2014-05-08 Afilias plc +lgbt + +// liaison : 2014-10-02 Liaison Technologies, Incorporated +liaison + +// lidl : 2014-09-18 Schwarz Domains und Services GmbH & Co. KG +lidl + +// life : 2014-02-06 Binky Moon, LLC +life + +// lifeinsurance : 2015-01-15 American Council of Life Insurers +lifeinsurance + +// lifestyle : 2014-12-11 Lifestyle Domain Holdings, Inc. +lifestyle + +// lighting : 2013-08-27 Binky Moon, LLC +lighting + +// like : 2014-12-18 Amazon Registry Services, Inc. +like + +// lilly : 2015-07-31 Eli Lilly and Company +lilly + +// limited : 2014-03-06 Binky Moon, LLC +limited + +// limo : 2013-10-17 Binky Moon, LLC +limo + +// lincoln : 2014-11-13 Ford Motor Company +lincoln + +// linde : 2014-12-04 Linde Aktiengesellschaft +linde + +// link : 2013-11-14 Uniregistry, Corp. +link + +// lipsy : 2015-06-25 Lipsy Ltd +lipsy + +// live : 2014-12-04 United TLD Holdco Ltd. +live + +// living : 2015-07-30 Lifestyle Domain Holdings, Inc. +living + +// lixil : 2015-03-19 LIXIL Group Corporation +lixil + +// llc : 2017-12-14 Afilias plc +llc + +// loan : 2014-11-20 dot Loan Limited +loan + +// loans : 2014-03-20 Binky Moon, LLC +loans + +// locker : 2015-06-04 Dish DBS Corporation +locker + +// locus : 2015-06-25 Locus Analytics LLC +locus + +// loft : 2015-07-30 Annco, Inc. +loft + +// lol : 2015-01-30 Uniregistry, Corp. +lol + +// london : 2013-11-14 Dot London Domains Limited +london + +// lotte : 2014-11-07 Lotte Holdings Co., Ltd. +lotte + +// lotto : 2014-04-10 Afilias plc +lotto + +// love : 2014-12-22 Merchant Law Group LLP +love + +// lpl : 2015-07-30 LPL Holdings, Inc. +lpl + +// lplfinancial : 2015-07-30 LPL Holdings, Inc. +lplfinancial + +// ltd : 2014-09-25 Binky Moon, LLC +ltd + +// ltda : 2014-04-17 InterNetX, Corp +ltda + +// lundbeck : 2015-08-06 H. Lundbeck A/S +lundbeck + +// lupin : 2014-11-07 LUPIN LIMITED +lupin + +// luxe : 2014-01-09 Minds + Machines Group Limited +luxe + +// luxury : 2013-10-17 Luxury Partners, LLC +luxury + +// macys : 2015-07-31 Macys, Inc. +macys + +// madrid : 2014-05-01 Comunidad de Madrid +madrid + +// maif : 2014-10-02 Mutuelle Assurance Instituteur France (MAIF) +maif + +// maison : 2013-12-05 Binky Moon, LLC +maison + +// makeup : 2015-01-15 L'Oréal +makeup + +// man : 2014-12-04 MAN SE +man + +// management : 2013-11-07 Binky Moon, LLC +management + +// mango : 2013-10-24 PUNTO FA S.L. +mango + +// map : 2016-06-09 Charleston Road Registry Inc. +map + +// market : 2014-03-06 United TLD Holdco Ltd. +market + +// marketing : 2013-11-07 Binky Moon, LLC +marketing + +// markets : 2014-12-11 Dotmarkets Registry Limited +markets + +// marriott : 2014-10-09 Marriott Worldwide Corporation +marriott + +// marshalls : 2015-07-16 The TJX Companies, Inc. +marshalls + +// maserati : 2015-07-31 Fiat Chrysler Automobiles N.V. +maserati + +// mattel : 2015-08-06 Mattel Sites, Inc. +mattel + +// mba : 2015-04-02 Binky Moon, LLC +mba + +// mckinsey : 2015-07-31 McKinsey Holdings, Inc. +mckinsey + +// med : 2015-08-06 Medistry LLC +med + +// media : 2014-03-06 Binky Moon, LLC +media + +// meet : 2014-01-16 Charleston Road Registry Inc. +meet + +// melbourne : 2014-05-29 The Crown in right of the State of Victoria, represented by its Department of State Development, Business and Innovation +melbourne + +// meme : 2014-01-30 Charleston Road Registry Inc. +meme + +// memorial : 2014-10-16 Dog Beach, LLC +memorial + +// men : 2015-02-26 Exclusive Registry Limited +men + +// menu : 2013-09-11 Wedding TLD2, LLC +menu + +// merckmsd : 2016-07-14 MSD Registry Holdings, Inc. +merckmsd + +// metlife : 2015-05-07 MetLife Services and Solutions, LLC +metlife + +// miami : 2013-12-19 Minds + Machines Group Limited +miami + +// microsoft : 2014-12-18 Microsoft Corporation +microsoft + +// mini : 2014-01-09 Bayerische Motoren Werke Aktiengesellschaft +mini + +// mint : 2015-07-30 Intuit Administrative Services, Inc. +mint + +// mit : 2015-07-02 Massachusetts Institute of Technology +mit + +// mitsubishi : 2015-07-23 Mitsubishi Corporation +mitsubishi + +// mlb : 2015-05-21 MLB Advanced Media DH, LLC +mlb + +// mls : 2015-04-23 The Canadian Real Estate Association +mls + +// mma : 2014-11-07 MMA IARD +mma + +// mobile : 2016-06-02 Dish DBS Corporation +mobile + +// mobily : 2014-12-18 GreenTech Consultancy Company W.L.L. +mobily + +// moda : 2013-11-07 United TLD Holdco Ltd. +moda + +// moe : 2013-11-13 Interlink Co., Ltd. +moe + +// moi : 2014-12-18 Amazon Registry Services, Inc. +moi + +// mom : 2015-04-16 Uniregistry, Corp. +mom + +// monash : 2013-09-30 Monash University +monash + +// money : 2014-10-16 Binky Moon, LLC +money + +// monster : 2015-09-11 Monster Worldwide, Inc. +monster + +// mopar : 2015-07-30 FCA US LLC. +mopar + +// mormon : 2013-12-05 IRI Domain Management, LLC ("Applicant") +mormon + +// mortgage : 2014-03-20 United TLD Holdco Ltd. +mortgage + +// moscow : 2013-12-19 Foundation for Assistance for Internet Technologies and Infrastructure Development (FAITID) +moscow + +// moto : 2015-06-04 Motorola Trademark Holdings, LLC +moto + +// motorcycles : 2014-01-09 DERMotorcycles, LLC +motorcycles + +// mov : 2014-01-30 Charleston Road Registry Inc. +mov + +// movie : 2015-02-05 Binky Moon, LLC +movie + +// movistar : 2014-10-16 Telefónica S.A. +movistar + +// msd : 2015-07-23 MSD Registry Holdings, Inc. +msd + +// mtn : 2014-12-04 MTN Dubai Limited +mtn + +// mtr : 2015-03-12 MTR Corporation Limited +mtr + +// mutual : 2015-04-02 Northwestern Mutual MU TLD Registry, LLC +mutual + +// nab : 2015-08-20 National Australia Bank Limited +nab + +// nadex : 2014-12-11 Nadex Domains, Inc. +nadex + +// nagoya : 2013-10-24 GMO Registry, Inc. +nagoya + +// nationwide : 2015-07-23 Nationwide Mutual Insurance Company +nationwide + +// natura : 2015-03-12 NATURA COSMÉTICOS S.A. +natura + +// navy : 2014-03-06 United TLD Holdco Ltd. +navy + +// nba : 2015-07-31 NBA REGISTRY, LLC +nba + +// nec : 2015-01-08 NEC Corporation +nec + +// netbank : 2014-06-26 COMMONWEALTH BANK OF AUSTRALIA +netbank + +// netflix : 2015-06-18 Netflix, Inc. +netflix + +// network : 2013-11-14 Binky Moon, LLC +network + +// neustar : 2013-12-05 Registry Services, LLC +neustar + +// new : 2014-01-30 Charleston Road Registry Inc. +new + +// newholland : 2015-09-03 CNH Industrial N.V. +newholland + +// news : 2014-12-18 United TLD Holdco Ltd. +news + +// next : 2015-06-18 Next plc +next + +// nextdirect : 2015-06-18 Next plc +nextdirect + +// nexus : 2014-07-24 Charleston Road Registry Inc. +nexus + +// nfl : 2015-07-23 NFL Reg Ops LLC +nfl + +// ngo : 2014-03-06 Public Interest Registry +ngo + +// nhk : 2014-02-13 Japan Broadcasting Corporation (NHK) +nhk + +// nico : 2014-12-04 DWANGO Co., Ltd. +nico + +// nike : 2015-07-23 NIKE, Inc. +nike + +// nikon : 2015-05-21 NIKON CORPORATION +nikon + +// ninja : 2013-11-07 United TLD Holdco Ltd. +ninja + +// nissan : 2014-03-27 NISSAN MOTOR CO., LTD. +nissan + +// nissay : 2015-10-29 Nippon Life Insurance Company +nissay + +// nokia : 2015-01-08 Nokia Corporation +nokia + +// northwesternmutual : 2015-06-18 Northwestern Mutual Registry, LLC +northwesternmutual + +// norton : 2014-12-04 Symantec Corporation +norton + +// now : 2015-06-25 Amazon Registry Services, Inc. +now + +// nowruz : 2014-09-04 Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. +nowruz + +// nowtv : 2015-05-14 Starbucks (HK) Limited +nowtv + +// nra : 2014-05-22 NRA Holdings Company, INC. +nra + +// nrw : 2013-11-21 Minds + Machines GmbH +nrw + +// ntt : 2014-10-31 NIPPON TELEGRAPH AND TELEPHONE CORPORATION +ntt + +// nyc : 2014-01-23 The City of New York by and through the New York City Department of Information Technology & Telecommunications +nyc + +// obi : 2014-09-25 OBI Group Holding SE & Co. KGaA +obi + +// observer : 2015-04-30 Top Level Spectrum, Inc. +observer + +// off : 2015-07-23 Johnson Shareholdings, Inc. +off + +// office : 2015-03-12 Microsoft Corporation +office + +// okinawa : 2013-12-05 BRregistry, Inc. +okinawa + +// olayan : 2015-05-14 Crescent Holding GmbH +olayan + +// olayangroup : 2015-05-14 Crescent Holding GmbH +olayangroup + +// oldnavy : 2015-07-31 The Gap, Inc. +oldnavy + +// ollo : 2015-06-04 Dish DBS Corporation +ollo + +// omega : 2015-01-08 The Swatch Group Ltd +omega + +// one : 2014-11-07 One.com A/S +one + +// ong : 2014-03-06 Public Interest Registry +ong + +// onl : 2013-09-16 I-Registry Ltd. +onl + +// online : 2015-01-15 DotOnline Inc. +online + +// onyourside : 2015-07-23 Nationwide Mutual Insurance Company +onyourside + +// ooo : 2014-01-09 INFIBEAM INCORPORATION LIMITED +ooo + +// open : 2015-07-31 American Express Travel Related Services Company, Inc. +open + +// oracle : 2014-06-19 Oracle Corporation +oracle + +// orange : 2015-03-12 Orange Brand Services Limited +orange + +// organic : 2014-03-27 Afilias plc +organic + +// origins : 2015-10-01 The Estée Lauder Companies Inc. +origins + +// osaka : 2014-09-04 Osaka Registry Co., Ltd. +osaka + +// otsuka : 2013-10-11 Otsuka Holdings Co., Ltd. +otsuka + +// ott : 2015-06-04 Dish DBS Corporation +ott + +// ovh : 2014-01-16 OVH SAS +ovh + +// page : 2014-12-04 Charleston Road Registry Inc. +page + +// panasonic : 2015-07-30 Panasonic Corporation +panasonic + +// panerai : 2014-11-07 Richemont DNS Inc. +panerai + +// paris : 2014-01-30 City of Paris +paris + +// pars : 2014-09-04 Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. +pars + +// partners : 2013-12-05 Binky Moon, LLC +partners + +// parts : 2013-12-05 Binky Moon, LLC +parts + +// party : 2014-09-11 Blue Sky Registry Limited +party + +// passagens : 2015-03-05 Travel Reservations SRL +passagens + +// pay : 2015-08-27 Amazon Registry Services, Inc. +pay + +// pccw : 2015-05-14 PCCW Enterprises Limited +pccw + +// pet : 2015-05-07 Afilias plc +pet + +// pfizer : 2015-09-11 Pfizer Inc. +pfizer + +// pharmacy : 2014-06-19 National Association of Boards of Pharmacy +pharmacy + +// phd : 2016-07-28 Charleston Road Registry Inc. +phd + +// philips : 2014-11-07 Koninklijke Philips N.V. +philips + +// phone : 2016-06-02 Dish DBS Corporation +phone + +// photo : 2013-11-14 Uniregistry, Corp. +photo + +// photography : 2013-09-20 Binky Moon, LLC +photography + +// photos : 2013-10-17 Binky Moon, LLC +photos + +// physio : 2014-05-01 PhysBiz Pty Ltd +physio + +// piaget : 2014-10-16 Richemont DNS Inc. +piaget + +// pics : 2013-11-14 Uniregistry, Corp. +pics + +// pictet : 2014-06-26 Pictet Europe S.A. +pictet + +// pictures : 2014-03-06 Binky Moon, LLC +pictures + +// pid : 2015-01-08 Top Level Spectrum, Inc. +pid + +// pin : 2014-12-18 Amazon Registry Services, Inc. +pin + +// ping : 2015-06-11 Ping Registry Provider, Inc. +ping + +// pink : 2013-10-01 Afilias plc +pink + +// pioneer : 2015-07-16 Pioneer Corporation +pioneer + +// pizza : 2014-06-26 Binky Moon, LLC +pizza + +// place : 2014-04-24 Binky Moon, LLC +place + +// play : 2015-03-05 Charleston Road Registry Inc. +play + +// playstation : 2015-07-02 Sony Computer Entertainment Inc. +playstation + +// plumbing : 2013-09-10 Binky Moon, LLC +plumbing + +// plus : 2015-02-05 Binky Moon, LLC +plus + +// pnc : 2015-07-02 PNC Domain Co., LLC +pnc + +// pohl : 2014-06-23 Deutsche Vermögensberatung Aktiengesellschaft DVAG +pohl + +// poker : 2014-07-03 Afilias plc +poker + +// politie : 2015-08-20 Politie Nederland +politie + +// porn : 2014-10-16 ICM Registry PN LLC +porn + +// pramerica : 2015-07-30 Prudential Financial, Inc. +pramerica + +// praxi : 2013-12-05 Praxi S.p.A. +praxi + +// press : 2014-04-03 DotPress Inc. +press + +// prime : 2015-06-25 Amazon Registry Services, Inc. +prime + +// prod : 2014-01-23 Charleston Road Registry Inc. +prod + +// productions : 2013-12-05 Binky Moon, LLC +productions + +// prof : 2014-07-24 Charleston Road Registry Inc. +prof + +// progressive : 2015-07-23 Progressive Casualty Insurance Company +progressive + +// promo : 2014-12-18 Afilias plc +promo + +// properties : 2013-12-05 Binky Moon, LLC +properties + +// property : 2014-05-22 Uniregistry, Corp. +property + +// protection : 2015-04-23 XYZ.COM LLC +protection + +// pru : 2015-07-30 Prudential Financial, Inc. +pru + +// prudential : 2015-07-30 Prudential Financial, Inc. +prudential + +// pub : 2013-12-12 United TLD Holdco Ltd. +pub + +// pwc : 2015-10-29 PricewaterhouseCoopers LLP +pwc + +// qpon : 2013-11-14 dotCOOL, Inc. +qpon + +// quebec : 2013-12-19 PointQuébec Inc +quebec + +// quest : 2015-03-26 Quest ION Limited +quest + +// qvc : 2015-07-30 QVC, Inc. +qvc + +// racing : 2014-12-04 Premier Registry Limited +racing + +// radio : 2016-07-21 European Broadcasting Union (EBU) +radio + +// raid : 2015-07-23 Johnson Shareholdings, Inc. +raid + +// read : 2014-12-18 Amazon Registry Services, Inc. +read + +// realestate : 2015-09-11 dotRealEstate LLC +realestate + +// realtor : 2014-05-29 Real Estate Domains LLC +realtor + +// realty : 2015-03-19 Fegistry, LLC +realty + +// recipes : 2013-10-17 Binky Moon, LLC +recipes + +// red : 2013-11-07 Afilias plc +red + +// redstone : 2014-10-31 Redstone Haute Couture Co., Ltd. +redstone + +// redumbrella : 2015-03-26 Travelers TLD, LLC +redumbrella + +// rehab : 2014-03-06 United TLD Holdco Ltd. +rehab + +// reise : 2014-03-13 Binky Moon, LLC +reise + +// reisen : 2014-03-06 Binky Moon, LLC +reisen + +// reit : 2014-09-04 National Association of Real Estate Investment Trusts, Inc. +reit + +// reliance : 2015-04-02 Reliance Industries Limited +reliance + +// ren : 2013-12-12 Beijing Qianxiang Wangjing Technology Development Co., Ltd. +ren + +// rent : 2014-12-04 XYZ.COM LLC +rent + +// rentals : 2013-12-05 Binky Moon, LLC +rentals + +// repair : 2013-11-07 Binky Moon, LLC +repair + +// report : 2013-12-05 Binky Moon, LLC +report + +// republican : 2014-03-20 United TLD Holdco Ltd. +republican + +// rest : 2013-12-19 Punto 2012 Sociedad Anonima Promotora de Inversion de Capital Variable +rest + +// restaurant : 2014-07-03 Binky Moon, LLC +restaurant + +// review : 2014-11-20 dot Review Limited +review + +// reviews : 2013-09-13 United TLD Holdco Ltd. +reviews + +// rexroth : 2015-06-18 Robert Bosch GMBH +rexroth + +// rich : 2013-11-21 I-Registry Ltd. +rich + +// richardli : 2015-05-14 Pacific Century Asset Management (HK) Limited +richardli + +// ricoh : 2014-11-20 Ricoh Company, Ltd. +ricoh + +// rightathome : 2015-07-23 Johnson Shareholdings, Inc. +rightathome + +// ril : 2015-04-02 Reliance Industries Limited +ril + +// rio : 2014-02-27 Empresa Municipal de Informática SA - IPLANRIO +rio + +// rip : 2014-07-10 United TLD Holdco Ltd. +rip + +// rmit : 2015-11-19 Royal Melbourne Institute of Technology +rmit + +// rocher : 2014-12-18 Ferrero Trading Lux S.A. +rocher + +// rocks : 2013-11-14 United TLD Holdco Ltd. +rocks + +// rodeo : 2013-12-19 Minds + Machines Group Limited +rodeo + +// rogers : 2015-08-06 Rogers Communications Canada Inc. +rogers + +// room : 2014-12-18 Amazon Registry Services, Inc. +room + +// rsvp : 2014-05-08 Charleston Road Registry Inc. +rsvp + +// rugby : 2016-12-15 World Rugby Strategic Developments Limited +rugby + +// ruhr : 2013-10-02 regiodot GmbH & Co. KG +ruhr + +// run : 2015-03-19 Binky Moon, LLC +run + +// rwe : 2015-04-02 RWE AG +rwe + +// ryukyu : 2014-01-09 BRregistry, Inc. +ryukyu + +// saarland : 2013-12-12 dotSaarland GmbH +saarland + +// safe : 2014-12-18 Amazon Registry Services, Inc. +safe + +// safety : 2015-01-08 Safety Registry Services, LLC. +safety + +// sakura : 2014-12-18 SAKURA Internet Inc. +sakura + +// sale : 2014-10-16 United TLD Holdco Ltd. +sale + +// salon : 2014-12-11 Binky Moon, LLC +salon + +// samsclub : 2015-07-31 Wal-Mart Stores, Inc. +samsclub + +// samsung : 2014-04-03 SAMSUNG SDS CO., LTD +samsung + +// sandvik : 2014-11-13 Sandvik AB +sandvik + +// sandvikcoromant : 2014-11-07 Sandvik AB +sandvikcoromant + +// sanofi : 2014-10-09 Sanofi +sanofi + +// sap : 2014-03-27 SAP AG +sap + +// sarl : 2014-07-03 Binky Moon, LLC +sarl + +// sas : 2015-04-02 Research IP LLC +sas + +// save : 2015-06-25 Amazon Registry Services, Inc. +save + +// saxo : 2014-10-31 Saxo Bank A/S +saxo + +// sbi : 2015-03-12 STATE BANK OF INDIA +sbi + +// sbs : 2014-11-07 SPECIAL BROADCASTING SERVICE CORPORATION +sbs + +// sca : 2014-03-13 SVENSKA CELLULOSA AKTIEBOLAGET SCA (publ) +sca + +// scb : 2014-02-20 The Siam Commercial Bank Public Company Limited ("SCB") +scb + +// schaeffler : 2015-08-06 Schaeffler Technologies AG & Co. KG +schaeffler + +// schmidt : 2014-04-03 SALM S.A.S. +schmidt + +// scholarships : 2014-04-24 Scholarships.com, LLC +scholarships + +// school : 2014-12-18 Binky Moon, LLC +school + +// schule : 2014-03-06 Binky Moon, LLC +schule + +// schwarz : 2014-09-18 Schwarz Domains und Services GmbH & Co. KG +schwarz + +// science : 2014-09-11 dot Science Limited +science + +// scjohnson : 2015-07-23 Johnson Shareholdings, Inc. +scjohnson + +// scor : 2014-10-31 SCOR SE +scor + +// scot : 2014-01-23 Dot Scot Registry Limited +scot + +// search : 2016-06-09 Charleston Road Registry Inc. +search + +// seat : 2014-05-22 SEAT, S.A. (Sociedad Unipersonal) +seat + +// secure : 2015-08-27 Amazon Registry Services, Inc. +secure + +// security : 2015-05-14 XYZ.COM LLC +security + +// seek : 2014-12-04 Seek Limited +seek + +// select : 2015-10-08 iSelect Ltd +select + +// sener : 2014-10-24 Sener Ingeniería y Sistemas, S.A. +sener + +// services : 2014-02-27 Binky Moon, LLC +services + +// ses : 2015-07-23 SES +ses + +// seven : 2015-08-06 Seven West Media Ltd +seven + +// sew : 2014-07-17 SEW-EURODRIVE GmbH & Co KG +sew + +// sex : 2014-11-13 ICM Registry SX LLC +sex + +// sexy : 2013-09-11 Uniregistry, Corp. +sexy + +// sfr : 2015-08-13 Societe Francaise du Radiotelephone - SFR +sfr + +// shangrila : 2015-09-03 Shangri‐La International Hotel Management Limited +shangrila + +// sharp : 2014-05-01 Sharp Corporation +sharp + +// shaw : 2015-04-23 Shaw Cablesystems G.P. +shaw + +// shell : 2015-07-30 Shell Information Technology International Inc +shell + +// shia : 2014-09-04 Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. +shia + +// shiksha : 2013-11-14 Afilias plc +shiksha + +// shoes : 2013-10-02 Binky Moon, LLC +shoes + +// shop : 2016-04-08 GMO Registry, Inc. +shop + +// shopping : 2016-03-31 Binky Moon, LLC +shopping + +// shouji : 2015-01-08 QIHOO 360 TECHNOLOGY CO. LTD. +shouji + +// show : 2015-03-05 Binky Moon, LLC +show + +// showtime : 2015-08-06 CBS Domains Inc. +showtime + +// shriram : 2014-01-23 Shriram Capital Ltd. +shriram + +// silk : 2015-06-25 Amazon Registry Services, Inc. +silk + +// sina : 2015-03-12 Sina Corporation +sina + +// singles : 2013-08-27 Binky Moon, LLC +singles + +// site : 2015-01-15 DotSite Inc. +site + +// ski : 2015-04-09 Afilias plc +ski + +// skin : 2015-01-15 L'Oréal +skin + +// sky : 2014-06-19 Sky International AG +sky + +// skype : 2014-12-18 Microsoft Corporation +skype + +// sling : 2015-07-30 Hughes Satellite Systems Corporation +sling + +// smart : 2015-07-09 Smart Communications, Inc. (SMART) +smart + +// smile : 2014-12-18 Amazon Registry Services, Inc. +smile + +// sncf : 2015-02-19 Société Nationale des Chemins de fer Francais S N C F +sncf + +// soccer : 2015-03-26 Binky Moon, LLC +soccer + +// social : 2013-11-07 United TLD Holdco Ltd. +social + +// softbank : 2015-07-02 SoftBank Corp. +softbank + +// software : 2014-03-20 United TLD Holdco Ltd. +software + +// sohu : 2013-12-19 Sohu.com Limited +sohu + +// solar : 2013-11-07 Binky Moon, LLC +solar + +// solutions : 2013-11-07 Binky Moon, LLC +solutions + +// song : 2015-02-26 Amazon Registry Services, Inc. +song + +// sony : 2015-01-08 Sony Corporation +sony + +// soy : 2014-01-23 Charleston Road Registry Inc. +soy + +// space : 2014-04-03 DotSpace Inc. +space + +// spiegel : 2014-02-05 SPIEGEL-Verlag Rudolf Augstein GmbH & Co. KG +spiegel + +// sport : 2017-11-16 Global Association of International Sports Federations (GAISF) +sport + +// spot : 2015-02-26 Amazon Registry Services, Inc. +spot + +// spreadbetting : 2014-12-11 Dotspreadbetting Registry Limited +spreadbetting + +// srl : 2015-05-07 InterNetX, Corp +srl + +// srt : 2015-07-30 FCA US LLC. +srt + +// stada : 2014-11-13 STADA Arzneimittel AG +stada + +// staples : 2015-07-30 Staples, Inc. +staples + +// star : 2015-01-08 Star India Private Limited +star + +// starhub : 2015-02-05 StarHub Ltd +starhub + +// statebank : 2015-03-12 STATE BANK OF INDIA +statebank + +// statefarm : 2015-07-30 State Farm Mutual Automobile Insurance Company +statefarm + +// statoil : 2014-12-04 Statoil ASA +statoil + +// stc : 2014-10-09 Saudi Telecom Company +stc + +// stcgroup : 2014-10-09 Saudi Telecom Company +stcgroup + +// stockholm : 2014-12-18 Stockholms kommun +stockholm + +// storage : 2014-12-22 XYZ.COM LLC +storage + +// store : 2015-04-09 DotStore Inc. +store + +// stream : 2016-01-08 dot Stream Limited +stream + +// studio : 2015-02-11 United TLD Holdco Ltd. +studio + +// study : 2014-12-11 OPEN UNIVERSITIES AUSTRALIA PTY LTD +study + +// style : 2014-12-04 Binky Moon, LLC +style + +// sucks : 2014-12-22 Vox Populi Registry Ltd. +sucks + +// supplies : 2013-12-19 Binky Moon, LLC +supplies + +// supply : 2013-12-19 Binky Moon, LLC +supply + +// support : 2013-10-24 Binky Moon, LLC +support + +// surf : 2014-01-09 Minds + Machines Group Limited +surf + +// surgery : 2014-03-20 Binky Moon, LLC +surgery + +// suzuki : 2014-02-20 SUZUKI MOTOR CORPORATION +suzuki + +// swatch : 2015-01-08 The Swatch Group Ltd +swatch + +// swiftcover : 2015-07-23 Swiftcover Insurance Services Limited +swiftcover + +// swiss : 2014-10-16 Swiss Confederation +swiss + +// sydney : 2014-09-18 State of New South Wales, Department of Premier and Cabinet +sydney + +// symantec : 2014-12-04 Symantec Corporation +symantec + +// systems : 2013-11-07 Binky Moon, LLC +systems + +// tab : 2014-12-04 Tabcorp Holdings Limited +tab + +// taipei : 2014-07-10 Taipei City Government +taipei + +// talk : 2015-04-09 Amazon Registry Services, Inc. +talk + +// taobao : 2015-01-15 Alibaba Group Holding Limited +taobao + +// target : 2015-07-31 Target Domain Holdings, LLC +target + +// tatamotors : 2015-03-12 Tata Motors Ltd +tatamotors + +// tatar : 2014-04-24 Limited Liability Company "Coordination Center of Regional Domain of Tatarstan Republic" +tatar + +// tattoo : 2013-08-30 Uniregistry, Corp. +tattoo + +// tax : 2014-03-20 Binky Moon, LLC +tax + +// taxi : 2015-03-19 Binky Moon, LLC +taxi + +// tci : 2014-09-12 Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. +tci + +// tdk : 2015-06-11 TDK Corporation +tdk + +// team : 2015-03-05 Binky Moon, LLC +team + +// tech : 2015-01-30 Personals TLD Inc. +tech + +// technology : 2013-09-13 Binky Moon, LLC +technology + +// telecity : 2015-02-19 TelecityGroup International Limited +telecity + +// telefonica : 2014-10-16 Telefónica S.A. +telefonica + +// temasek : 2014-08-07 Temasek Holdings (Private) Limited +temasek + +// tennis : 2014-12-04 Binky Moon, LLC +tennis + +// teva : 2015-07-02 Teva Pharmaceutical Industries Limited +teva + +// thd : 2015-04-02 Home Depot Product Authority, LLC +thd + +// theater : 2015-03-19 Binky Moon, LLC +theater + +// theatre : 2015-05-07 XYZ.COM LLC +theatre + +// tiaa : 2015-07-23 Teachers Insurance and Annuity Association of America +tiaa + +// tickets : 2015-02-05 Accent Media Limited +tickets + +// tienda : 2013-11-14 Binky Moon, LLC +tienda + +// tiffany : 2015-01-30 Tiffany and Company +tiffany + +// tips : 2013-09-20 Binky Moon, LLC +tips + +// tires : 2014-11-07 Binky Moon, LLC +tires + +// tirol : 2014-04-24 punkt Tirol GmbH +tirol + +// tjmaxx : 2015-07-16 The TJX Companies, Inc. +tjmaxx + +// tjx : 2015-07-16 The TJX Companies, Inc. +tjx + +// tkmaxx : 2015-07-16 The TJX Companies, Inc. +tkmaxx + +// tmall : 2015-01-15 Alibaba Group Holding Limited +tmall + +// today : 2013-09-20 Binky Moon, LLC +today + +// tokyo : 2013-11-13 GMO Registry, Inc. +tokyo + +// tools : 2013-11-21 Binky Moon, LLC +tools + +// top : 2014-03-20 .TOP Registry +top + +// toray : 2014-12-18 Toray Industries, Inc. +toray + +// toshiba : 2014-04-10 TOSHIBA Corporation +toshiba + +// total : 2015-08-06 Total SA +total + +// tours : 2015-01-22 Binky Moon, LLC +tours + +// town : 2014-03-06 Binky Moon, LLC +town + +// toyota : 2015-04-23 TOYOTA MOTOR CORPORATION +toyota + +// toys : 2014-03-06 Binky Moon, LLC +toys + +// trade : 2014-01-23 Elite Registry Limited +trade + +// trading : 2014-12-11 Dottrading Registry Limited +trading + +// training : 2013-11-07 Binky Moon, LLC +training + +// travel : Dog Beach, LLC +travel + +// travelchannel : 2015-07-02 Lifestyle Domain Holdings, Inc. +travelchannel + +// travelers : 2015-03-26 Travelers TLD, LLC +travelers + +// travelersinsurance : 2015-03-26 Travelers TLD, LLC +travelersinsurance + +// trust : 2014-10-16 NCC Group Inc. +trust + +// trv : 2015-03-26 Travelers TLD, LLC +trv + +// tube : 2015-06-11 Latin American Telecom LLC +tube + +// tui : 2014-07-03 TUI AG +tui + +// tunes : 2015-02-26 Amazon Registry Services, Inc. +tunes + +// tushu : 2014-12-18 Amazon Registry Services, Inc. +tushu + +// tvs : 2015-02-19 T V SUNDRAM IYENGAR & SONS LIMITED +tvs + +// ubank : 2015-08-20 National Australia Bank Limited +ubank + +// ubs : 2014-12-11 UBS AG +ubs + +// uconnect : 2015-07-30 FCA US LLC. +uconnect + +// unicom : 2015-10-15 China United Network Communications Corporation Limited +unicom + +// university : 2014-03-06 Binky Moon, LLC +university + +// uno : 2013-09-11 Dot Latin LLC +uno + +// uol : 2014-05-01 UBN INTERNET LTDA. +uol + +// ups : 2015-06-25 UPS Market Driver, Inc. +ups + +// vacations : 2013-12-05 Binky Moon, LLC +vacations + +// vana : 2014-12-11 Lifestyle Domain Holdings, Inc. +vana + +// vanguard : 2015-09-03 The Vanguard Group, Inc. +vanguard + +// vegas : 2014-01-16 Dot Vegas, Inc. +vegas + +// ventures : 2013-08-27 Binky Moon, LLC +ventures + +// verisign : 2015-08-13 VeriSign, Inc. +verisign + +// versicherung : 2014-03-20 TLD-BOX Registrydienstleistungen GmbH +versicherung + +// vet : 2014-03-06 United TLD Holdco Ltd. +vet + +// viajes : 2013-10-17 Binky Moon, LLC +viajes + +// video : 2014-10-16 United TLD Holdco Ltd. +video + +// vig : 2015-05-14 VIENNA INSURANCE GROUP AG Wiener Versicherung Gruppe +vig + +// viking : 2015-04-02 Viking River Cruises (Bermuda) Ltd. +viking + +// villas : 2013-12-05 Binky Moon, LLC +villas + +// vin : 2015-06-18 Binky Moon, LLC +vin + +// vip : 2015-01-22 Minds + Machines Group Limited +vip + +// virgin : 2014-09-25 Virgin Enterprises Limited +virgin + +// visa : 2015-07-30 Visa Worldwide Pte. Limited +visa + +// vision : 2013-12-05 Binky Moon, LLC +vision + +// vista : 2014-09-18 Vistaprint Limited +vista + +// vistaprint : 2014-09-18 Vistaprint Limited +vistaprint + +// viva : 2014-11-07 Saudi Telecom Company +viva + +// vivo : 2015-07-31 Telefonica Brasil S.A. +vivo + +// vlaanderen : 2014-02-06 DNS.be vzw +vlaanderen + +// vodka : 2013-12-19 Minds + Machines Group Limited +vodka + +// volkswagen : 2015-05-14 Volkswagen Group of America Inc. +volkswagen + +// volvo : 2015-11-12 Volvo Holding Sverige Aktiebolag +volvo + +// vote : 2013-11-21 Monolith Registry LLC +vote + +// voting : 2013-11-13 Valuetainment Corp. +voting + +// voto : 2013-11-21 Monolith Registry LLC +voto + +// voyage : 2013-08-27 Binky Moon, LLC +voyage + +// vuelos : 2015-03-05 Travel Reservations SRL +vuelos + +// wales : 2014-05-08 Nominet UK +wales + +// walmart : 2015-07-31 Wal-Mart Stores, Inc. +walmart + +// walter : 2014-11-13 Sandvik AB +walter + +// wang : 2013-10-24 Zodiac Wang Limited +wang + +// wanggou : 2014-12-18 Amazon Registry Services, Inc. +wanggou + +// warman : 2015-06-18 Weir Group IP Limited +warman + +// watch : 2013-11-14 Binky Moon, LLC +watch + +// watches : 2014-12-22 Richemont DNS Inc. +watches + +// weather : 2015-01-08 International Business Machines Corporation +weather + +// weatherchannel : 2015-03-12 International Business Machines Corporation +weatherchannel + +// webcam : 2014-01-23 dot Webcam Limited +webcam + +// weber : 2015-06-04 Saint-Gobain Weber SA +weber + +// website : 2014-04-03 DotWebsite Inc. +website + +// wed : 2013-10-01 Atgron, Inc. +wed + +// wedding : 2014-04-24 Minds + Machines Group Limited +wedding + +// weibo : 2015-03-05 Sina Corporation +weibo + +// weir : 2015-01-29 Weir Group IP Limited +weir + +// whoswho : 2014-02-20 Who's Who Registry +whoswho + +// wien : 2013-10-28 punkt.wien GmbH +wien + +// wiki : 2013-11-07 Top Level Design, LLC +wiki + +// williamhill : 2014-03-13 William Hill Organization Limited +williamhill + +// win : 2014-11-20 First Registry Limited +win + +// windows : 2014-12-18 Microsoft Corporation +windows + +// wine : 2015-06-18 Binky Moon, LLC +wine + +// winners : 2015-07-16 The TJX Companies, Inc. +winners + +// wme : 2014-02-13 William Morris Endeavor Entertainment, LLC +wme + +// wolterskluwer : 2015-08-06 Wolters Kluwer N.V. +wolterskluwer + +// woodside : 2015-07-09 Woodside Petroleum Limited +woodside + +// work : 2013-12-19 Minds + Machines Group Limited +work + +// works : 2013-11-14 Binky Moon, LLC +works + +// world : 2014-06-12 Binky Moon, LLC +world + +// wow : 2015-10-08 Amazon Registry Services, Inc. +wow + +// wtc : 2013-12-19 World Trade Centers Association, Inc. +wtc + +// wtf : 2014-03-06 Binky Moon, LLC +wtf + +// xbox : 2014-12-18 Microsoft Corporation +xbox + +// xerox : 2014-10-24 Xerox DNHC LLC +xerox + +// xfinity : 2015-07-09 Comcast IP Holdings I, LLC +xfinity + +// xihuan : 2015-01-08 QIHOO 360 TECHNOLOGY CO. LTD. +xihuan + +// xin : 2014-12-11 Elegant Leader Limited +xin + +// xn--11b4c3d : 2015-01-15 VeriSign Sarl +कॉम + +// xn--1ck2e1b : 2015-02-26 Amazon Registry Services, Inc. +セール + +// xn--1qqw23a : 2014-01-09 Guangzhou YU Wei Information Technology Co., Ltd. +佛山 + +// xn--30rr7y : 2014-06-12 Excellent First Limited +慈善 + +// xn--3bst00m : 2013-09-13 Eagle Horizon Limited +集团 + +// xn--3ds443g : 2013-09-08 TLD REGISTRY LIMITED +在线 + +// xn--3oq18vl8pn36a : 2015-07-02 Volkswagen (China) Investment Co., Ltd. +大众汽车 + +// xn--3pxu8k : 2015-01-15 VeriSign Sarl +点看 + +// xn--42c2d9a : 2015-01-15 VeriSign Sarl +คอม + +// xn--45q11c : 2013-11-21 Zodiac Gemini Ltd +八卦 + +// xn--4gbrim : 2013-10-04 Suhub Electronic Establishment +موقع + +// xn--55qw42g : 2013-11-08 China Organizational Name Administration Center +公益 + +// xn--55qx5d : 2013-11-14 China Internet Network Information Center (CNNIC) +公司 + +// xn--5su34j936bgsg : 2015-09-03 Shangri‐La International Hotel Management Limited +香格里拉 + +// xn--5tzm5g : 2014-12-22 Global Website TLD Asia Limited +网站 + +// xn--6frz82g : 2013-09-23 Afilias plc +移动 + +// xn--6qq986b3xl : 2013-09-13 Tycoon Treasure Limited +我爱你 + +// xn--80adxhks : 2013-12-19 Foundation for Assistance for Internet Technologies and Infrastructure Development (FAITID) +москва + +// xn--80aqecdr1a : 2015-10-21 Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) +католик + +// xn--80asehdb : 2013-07-14 CORE Association +онлайн + +// xn--80aswg : 2013-07-14 CORE Association +сайт + +// xn--8y0a063a : 2015-03-26 China United Network Communications Corporation Limited +联通 + +// xn--9dbq2a : 2015-01-15 VeriSign Sarl +קום + +// xn--9et52u : 2014-06-12 RISE VICTORY LIMITED +时尚 + +// xn--9krt00a : 2015-03-12 Sina Corporation +微博 + +// xn--b4w605ferd : 2014-08-07 Temasek Holdings (Private) Limited +淡马锡 + +// xn--bck1b9a5dre4c : 2015-02-26 Amazon Registry Services, Inc. +ファッション + +// xn--c1avg : 2013-11-14 Public Interest Registry +орг + +// xn--c2br7g : 2015-01-15 VeriSign Sarl +नेट + +// xn--cck2b3b : 2015-02-26 Amazon Registry Services, Inc. +ストア + +// xn--cg4bki : 2013-09-27 SAMSUNG SDS CO., LTD +삼성 + +// xn--czr694b : 2014-01-16 Dot Trademark TLD Holding Company Limited +商标 + +// xn--czrs0t : 2013-12-19 Binky Moon, LLC +商店 + +// xn--czru2d : 2013-11-21 Zodiac Aquarius Limited +商城 + +// xn--d1acj3b : 2013-11-20 The Foundation for Network Initiatives “The Smart Internet” +дети + +// xn--eckvdtc9d : 2014-12-18 Amazon Registry Services, Inc. +ポイント + +// xn--efvy88h : 2014-08-22 Guangzhou YU Wei Information Technology Co., Ltd. +新闻 + +// xn--estv75g : 2015-02-19 Industrial and Commercial Bank of China Limited +工行 + +// xn--fct429k : 2015-04-09 Amazon Registry Services, Inc. +家電 + +// xn--fhbei : 2015-01-15 VeriSign Sarl +كوم + +// xn--fiq228c5hs : 2013-09-08 TLD REGISTRY LIMITED +中文网 + +// xn--fiq64b : 2013-10-14 CITIC Group Corporation +中信 + +// xn--fjq720a : 2014-05-22 Binky Moon, LLC +娱乐 + +// xn--flw351e : 2014-07-31 Charleston Road Registry Inc. +谷歌 + +// xn--fzys8d69uvgm : 2015-05-14 PCCW Enterprises Limited +電訊盈科 + +// xn--g2xx48c : 2015-01-30 Minds + Machines Group Limited +购物 + +// xn--gckr3f0f : 2015-02-26 Amazon Registry Services, Inc. +クラウド + +// xn--gk3at1e : 2015-10-08 Amazon Registry Services, Inc. +通販 + +// xn--hxt814e : 2014-05-15 Zodiac Taurus Limited +网店 + +// xn--i1b6b1a6a2e : 2013-11-14 Public Interest Registry +संगठन + +// xn--imr513n : 2014-12-11 Dot Trademark TLD Holding Company Limited +餐厅 + +// xn--io0a7i : 2013-11-14 China Internet Network Information Center (CNNIC) +网络 + +// xn--j1aef : 2015-01-15 VeriSign Sarl +ком + +// xn--jlq61u9w7b : 2015-01-08 Nokia Corporation +诺基亚 + +// xn--jvr189m : 2015-02-26 Amazon Registry Services, Inc. +食品 + +// xn--kcrx77d1x4a : 2014-11-07 Koninklijke Philips N.V. +飞利浦 + +// xn--kpu716f : 2014-12-22 Richemont DNS Inc. +手表 + +// xn--kput3i : 2014-02-13 Beijing RITT-Net Technology Development Co., Ltd +手机 + +// xn--mgba3a3ejt : 2014-11-20 Aramco Services Company +ارامكو + +// xn--mgba7c0bbn0a : 2015-05-14 Crescent Holding GmbH +العليان + +// xn--mgbaakc7dvf : 2015-09-03 Emirates Telecommunications Corporation (trading as Etisalat) +اتصالات + +// xn--mgbab2bd : 2013-10-31 CORE Association +بازار + +// xn--mgbb9fbpob : 2014-12-18 GreenTech Consultancy Company W.L.L. +موبايلي + +// xn--mgbca7dzdo : 2015-07-30 Abu Dhabi Systems and Information Centre +ابوظبي + +// xn--mgbi4ecexp : 2015-10-21 Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) +كاثوليك + +// xn--mgbt3dhd : 2014-09-04 Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. +همراه + +// xn--mk1bu44c : 2015-01-15 VeriSign Sarl +닷컴 + +// xn--mxtq1m : 2014-03-06 Net-Chinese Co., Ltd. +政府 + +// xn--ngbc5azd : 2013-07-13 International Domain Registry Pty. Ltd. +شبكة + +// xn--ngbe9e0a : 2014-12-04 Kuwait Finance House +بيتك + +// xn--ngbrx : 2015-11-12 League of Arab States +عرب + +// xn--nqv7f : 2013-11-14 Public Interest Registry +机构 + +// xn--nqv7fs00ema : 2013-11-14 Public Interest Registry +组织机构 + +// xn--nyqy26a : 2014-11-07 Stable Tone Limited +健康 + +// xn--otu796d : 2017-08-06 Dot Trademark TLD Holding Company Limited +招聘 + +// xn--p1acf : 2013-12-12 Rusnames Limited +рус + +// xn--pbt977c : 2014-12-22 Richemont DNS Inc. +珠宝 + +// xn--pssy2u : 2015-01-15 VeriSign Sarl +大拿 + +// xn--q9jyb4c : 2013-09-17 Charleston Road Registry Inc. +みんな + +// xn--qcka1pmc : 2014-07-31 Charleston Road Registry Inc. +グーグル + +// xn--rhqv96g : 2013-09-11 Stable Tone Limited +世界 + +// xn--rovu88b : 2015-02-26 Amazon Registry Services, Inc. +書籍 + +// xn--ses554g : 2014-01-16 KNET Co., Ltd. +网址 + +// xn--t60b56a : 2015-01-15 VeriSign Sarl +닷넷 + +// xn--tckwe : 2015-01-15 VeriSign Sarl +コム + +// xn--tiq49xqyj : 2015-10-21 Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) +天主教 + +// xn--unup4y : 2013-07-14 Binky Moon, LLC +游戏 + +// xn--vermgensberater-ctb : 2014-06-23 Deutsche Vermögensberatung Aktiengesellschaft DVAG +vermögensberater + +// xn--vermgensberatung-pwb : 2014-06-23 Deutsche Vermögensberatung Aktiengesellschaft DVAG +vermögensberatung + +// xn--vhquv : 2013-08-27 Binky Moon, LLC +企业 + +// xn--vuq861b : 2014-10-16 Beijing Tele-info Network Technology Co., Ltd. +信息 + +// xn--w4r85el8fhu5dnra : 2015-04-30 Kerry Trading Co. Limited +嘉里大酒店 + +// xn--w4rs40l : 2015-07-30 Kerry Trading Co. Limited +嘉里 + +// xn--xhq521b : 2013-11-14 Guangzhou YU Wei Information Technology Co., Ltd. +广东 + +// xn--zfr164b : 2013-11-08 China Organizational Name Administration Center +政务 + +// xyz : 2013-12-05 XYZ.COM LLC +xyz + +// yachts : 2014-01-09 DERYachts, LLC +yachts + +// yahoo : 2015-04-02 Yahoo! Domain Services Inc. +yahoo + +// yamaxun : 2014-12-18 Amazon Registry Services, Inc. +yamaxun + +// yandex : 2014-04-10 YANDEX, LLC +yandex + +// yodobashi : 2014-11-20 YODOBASHI CAMERA CO.,LTD. +yodobashi + +// yoga : 2014-05-29 Minds + Machines Group Limited +yoga + +// yokohama : 2013-12-12 GMO Registry, Inc. +yokohama + +// you : 2015-04-09 Amazon Registry Services, Inc. +you + +// youtube : 2014-05-01 Charleston Road Registry Inc. +youtube + +// yun : 2015-01-08 QIHOO 360 TECHNOLOGY CO. LTD. +yun + +// zappos : 2015-06-25 Amazon Registry Services, Inc. +zappos + +// zara : 2014-11-07 Industria de Diseño Textil, S.A. (INDITEX, S.A.) +zara + +// zero : 2014-12-18 Amazon Registry Services, Inc. +zero + +// zip : 2014-05-08 Charleston Road Registry Inc. +zip + +// zippo : 2015-07-02 Zadco Company +zippo + +// zone : 2013-11-14 Binky Moon, LLC +zone + +// zuerich : 2014-11-07 Kanton Zürich (Canton of Zurich) +zuerich + + +// ===END ICANN DOMAINS=== +// ===BEGIN PRIVATE DOMAINS=== +// (Note: these are in alphabetical order by company name) + +// 1GB LLC : https://www.1gb.ua/ +// Submitted by 1GB LLC +cc.ua +inf.ua +ltd.ua + +// Agnat sp. z o.o. : https://domena.pl +// Submitted by Przemyslaw Plewa +beep.pl + +// Alces Software Ltd : http://alces-software.com +// Submitted by Mark J. Titorenko +*.compute.estate +*.alces.network + +// alwaysdata : https://www.alwaysdata.com +// Submitted by Cyril +alwaysdata.net + +// Amazon CloudFront : https://aws.amazon.com/cloudfront/ +// Submitted by Donavan Miller +cloudfront.net + +// Amazon Elastic Compute Cloud : https://aws.amazon.com/ec2/ +// Submitted by Luke Wells +*.compute.amazonaws.com +*.compute-1.amazonaws.com +*.compute.amazonaws.com.cn +us-east-1.amazonaws.com + +// Amazon Elastic Beanstalk : https://aws.amazon.com/elasticbeanstalk/ +// Submitted by Luke Wells +cn-north-1.eb.amazonaws.com.cn +elasticbeanstalk.com +ap-northeast-1.elasticbeanstalk.com +ap-northeast-2.elasticbeanstalk.com +ap-northeast-3.elasticbeanstalk.com +ap-south-1.elasticbeanstalk.com +ap-southeast-1.elasticbeanstalk.com +ap-southeast-2.elasticbeanstalk.com +ca-central-1.elasticbeanstalk.com +eu-central-1.elasticbeanstalk.com +eu-west-1.elasticbeanstalk.com +eu-west-2.elasticbeanstalk.com +eu-west-3.elasticbeanstalk.com +sa-east-1.elasticbeanstalk.com +us-east-1.elasticbeanstalk.com +us-east-2.elasticbeanstalk.com +us-gov-west-1.elasticbeanstalk.com +us-west-1.elasticbeanstalk.com +us-west-2.elasticbeanstalk.com + +// Amazon Elastic Load Balancing : https://aws.amazon.com/elasticloadbalancing/ +// Submitted by Luke Wells +*.elb.amazonaws.com +*.elb.amazonaws.com.cn + +// Amazon S3 : https://aws.amazon.com/s3/ +// Submitted by Luke Wells +s3.amazonaws.com +s3-ap-northeast-1.amazonaws.com +s3-ap-northeast-2.amazonaws.com +s3-ap-south-1.amazonaws.com +s3-ap-southeast-1.amazonaws.com +s3-ap-southeast-2.amazonaws.com +s3-ca-central-1.amazonaws.com +s3-eu-central-1.amazonaws.com +s3-eu-west-1.amazonaws.com +s3-eu-west-2.amazonaws.com +s3-eu-west-3.amazonaws.com +s3-external-1.amazonaws.com +s3-fips-us-gov-west-1.amazonaws.com +s3-sa-east-1.amazonaws.com +s3-us-gov-west-1.amazonaws.com +s3-us-east-2.amazonaws.com +s3-us-west-1.amazonaws.com +s3-us-west-2.amazonaws.com +s3.ap-northeast-2.amazonaws.com +s3.ap-south-1.amazonaws.com +s3.cn-north-1.amazonaws.com.cn +s3.ca-central-1.amazonaws.com +s3.eu-central-1.amazonaws.com +s3.eu-west-2.amazonaws.com +s3.eu-west-3.amazonaws.com +s3.us-east-2.amazonaws.com +s3.dualstack.ap-northeast-1.amazonaws.com +s3.dualstack.ap-northeast-2.amazonaws.com +s3.dualstack.ap-south-1.amazonaws.com +s3.dualstack.ap-southeast-1.amazonaws.com +s3.dualstack.ap-southeast-2.amazonaws.com +s3.dualstack.ca-central-1.amazonaws.com +s3.dualstack.eu-central-1.amazonaws.com +s3.dualstack.eu-west-1.amazonaws.com +s3.dualstack.eu-west-2.amazonaws.com +s3.dualstack.eu-west-3.amazonaws.com +s3.dualstack.sa-east-1.amazonaws.com +s3.dualstack.us-east-1.amazonaws.com +s3.dualstack.us-east-2.amazonaws.com +s3-website-us-east-1.amazonaws.com +s3-website-us-west-1.amazonaws.com +s3-website-us-west-2.amazonaws.com +s3-website-ap-northeast-1.amazonaws.com +s3-website-ap-southeast-1.amazonaws.com +s3-website-ap-southeast-2.amazonaws.com +s3-website-eu-west-1.amazonaws.com +s3-website-sa-east-1.amazonaws.com +s3-website.ap-northeast-2.amazonaws.com +s3-website.ap-south-1.amazonaws.com +s3-website.ca-central-1.amazonaws.com +s3-website.eu-central-1.amazonaws.com +s3-website.eu-west-2.amazonaws.com +s3-website.eu-west-3.amazonaws.com +s3-website.us-east-2.amazonaws.com + +// Amune : https://amune.org/ +// Submitted by Team Amune +t3l3p0rt.net +tele.amune.org + +// Aptible : https://www.aptible.com/ +// Submitted by Thomas Orozco +on-aptible.com + +// Asociación Amigos de la Informática "Euskalamiga" : http://encounter.eus/ +// Submitted by Hector Martin +user.party.eus + +// Association potager.org : https://potager.org/ +// Submitted by Lunar +pimienta.org +poivron.org +potager.org +sweetpepper.org + +// ASUSTOR Inc. : http://www.asustor.com +// Submitted by Vincent Tseng +myasustor.com + +// AVM : https://avm.de +// Submitted by Andreas Weise +myfritz.net + +// AW AdvisorWebsites.com Software Inc : https://advisorwebsites.com +// Submitted by James Kennedy +*.awdev.ca +*.advisor.ws + +// backplane : https://www.backplane.io +// Submitted by Anthony Voutas +backplaneapp.io + +// BetaInABox +// Submitted by Adrian +betainabox.com + +// BinaryLane : http://www.binarylane.com +// Submitted by Nathan O'Sullivan +bnr.la + +// Blackbaud, Inc. : https://www.blackbaud.com +// Submitted by Paul Crowder +blackbaudcdn.net + +// Boomla : https://boomla.com +// Submitted by Tibor Halter +boomla.net + +// Boxfuse : https://boxfuse.com +// Submitted by Axel Fontaine +boxfuse.io + +// bplaced : https://www.bplaced.net/ +// Submitted by Miroslav Bozic +square7.ch +bplaced.com +bplaced.de +square7.de +bplaced.net +square7.net + +// BrowserSafetyMark +// Submitted by Dave Tharp +browsersafetymark.io + +// callidomus : https://www.callidomus.com/ +// Submitted by Marcus Popp +mycd.eu + +// CentralNic : http://www.centralnic.com/names/domains +// Submitted by registry +ae.org +ar.com +br.com +cn.com +com.de +com.se +de.com +eu.com +gb.com +gb.net +hu.com +hu.net +jp.net +jpn.com +kr.com +mex.com +no.com +qc.com +ru.com +sa.com +se.net +uk.com +uk.net +us.com +uy.com +za.bz +za.com + +// Africa.com Web Solutions Ltd : https://registry.africa.com +// Submitted by Gavin Brown +africa.com + +// iDOT Services Limited : http://www.domain.gr.com +// Submitted by Gavin Brown +gr.com + +// Radix FZC : http://domains.in.net +// Submitted by Gavin Brown +in.net + +// US REGISTRY LLC : http://us.org +// Submitted by Gavin Brown +us.org + +// co.com Registry, LLC : https://registry.co.com +// Submitted by Gavin Brown +co.com + +// c.la : http://www.c.la/ +c.la + +// certmgr.org : https://certmgr.org +// Submitted by B. Blechschmidt +certmgr.org + +// Citrix : https://citrix.com +// Submitted by Alex Stoddard +xenapponazure.com + +// ClearVox : http://www.clearvox.nl/ +// Submitted by Leon Rowland +virtueeldomein.nl + +// Clever Cloud : https://www.clever-cloud.com/ +// Submitted by Quentin Adam +cleverapps.io + +// Cloud66 : https://www.cloud66.com/ +// Submitted by Khash Sajadi +c66.me +cloud66.ws + +// CloudAccess.net : https://www.cloudaccess.net/ +// Submitted by Pawel Panek +jdevcloud.com +wpdevcloud.com +cloudaccess.host +freesite.host +cloudaccess.net + +// cloudControl : https://www.cloudcontrol.com/ +// Submitted by Tobias Wilken +cloudcontrolled.com +cloudcontrolapp.com + +// co.ca : http://registry.co.ca/ +co.ca + +// Co & Co : https://co-co.nl/ +// Submitted by Govert Versluis +*.otap.co + +// i-registry s.r.o. : http://www.i-registry.cz/ +// Submitted by Martin Semrad +co.cz + +// CDN77.com : http://www.cdn77.com +// Submitted by Jan Krpes +c.cdn77.org +cdn77-ssl.net +r.cdn77.net +rsc.cdn77.org +ssl.origin.cdn77-secure.org + +// Cloud DNS Ltd : http://www.cloudns.net +// Submitted by Aleksander Hristov +cloudns.asia +cloudns.biz +cloudns.club +cloudns.cc +cloudns.eu +cloudns.in +cloudns.info +cloudns.org +cloudns.pro +cloudns.pw +cloudns.us + +// Cloudeity Inc : https://cloudeity.com +// Submitted by Stefan Dimitrov +cloudeity.net + +// CNPY : https://cnpy.gdn +// Submitted by Angelo Gladding +cnpy.gdn + +// CoDNS B.V. +co.nl +co.no + +// Combell.com : https://www.combell.com +// Submitted by Thomas Wouters +webhosting.be +hosting-cluster.nl + +// COSIMO GmbH : http://www.cosimo.de +// Submitted by Rene Marticke +dyn.cosidns.de +dynamisches-dns.de +dnsupdater.de +internet-dns.de +l-o-g-i-n.de +dynamic-dns.info +feste-ip.net +knx-server.net +static-access.net + +// Craynic, s.r.o. : http://www.craynic.com/ +// Submitted by Ales Krajnik +realm.cz + +// Cryptonomic : https://cryptonomic.net/ +// Submitted by Andrew Cady +*.cryptonomic.net + +// Cupcake : https://cupcake.io/ +// Submitted by Jonathan Rudenberg +cupcake.is + +// cyon GmbH : https://www.cyon.ch/ +// Submitted by Dominic Luechinger +cyon.link +cyon.site + +// Daplie, Inc : https://daplie.com +// Submitted by AJ ONeal +daplie.me +localhost.daplie.me + +// Datto, Inc. : https://www.datto.com/ +// Submitted by Philipp Heckel +dattolocal.com +dattorelay.com +dattoweb.com +mydatto.com +dattolocal.net +mydatto.net + +// Dansk.net : http://www.dansk.net/ +// Submitted by Anani Voule +biz.dk +co.dk +firm.dk +reg.dk +store.dk + +// Debian : https://www.debian.org/ +// Submitted by Peter Palfrader / Debian Sysadmin Team +debian.net + +// deSEC : https://desec.io/ +// Submitted by Peter Thomassen +dedyn.io + +// DNShome : https://www.dnshome.de/ +// Submitted by Norbert Auler +dnshome.de + +// DrayTek Corp. : https://www.draytek.com/ +// Submitted by Paul Fang +drayddns.com + +// DreamHost : http://www.dreamhost.com/ +// Submitted by Andrew Farmer +dreamhosters.com + +// Drobo : http://www.drobo.com/ +// Submitted by Ricardo Padilha +mydrobo.com + +// Drud Holdings, LLC. : https://www.drud.com/ +// Submitted by Kevin Bridges +drud.io +drud.us + +// DuckDNS : http://www.duckdns.org/ +// Submitted by Richard Harper +duckdns.org + +// dy.fi : http://dy.fi/ +// Submitted by Heikki Hannikainen +dy.fi +tunk.org + +// DynDNS.com : http://www.dyndns.com/services/dns/dyndns/ +dyndns-at-home.com +dyndns-at-work.com +dyndns-blog.com +dyndns-free.com +dyndns-home.com +dyndns-ip.com +dyndns-mail.com +dyndns-office.com +dyndns-pics.com +dyndns-remote.com +dyndns-server.com +dyndns-web.com +dyndns-wiki.com +dyndns-work.com +dyndns.biz +dyndns.info +dyndns.org +dyndns.tv +at-band-camp.net +ath.cx +barrel-of-knowledge.info +barrell-of-knowledge.info +better-than.tv +blogdns.com +blogdns.net +blogdns.org +blogsite.org +boldlygoingnowhere.org +broke-it.net +buyshouses.net +cechire.com +dnsalias.com +dnsalias.net +dnsalias.org +dnsdojo.com +dnsdojo.net +dnsdojo.org +does-it.net +doesntexist.com +doesntexist.org +dontexist.com +dontexist.net +dontexist.org +doomdns.com +doomdns.org +dvrdns.org +dyn-o-saur.com +dynalias.com +dynalias.net +dynalias.org +dynathome.net +dyndns.ws +endofinternet.net +endofinternet.org +endoftheinternet.org +est-a-la-maison.com +est-a-la-masion.com +est-le-patron.com +est-mon-blogueur.com +for-better.biz +for-more.biz +for-our.info +for-some.biz +for-the.biz +forgot.her.name +forgot.his.name +from-ak.com +from-al.com +from-ar.com +from-az.net +from-ca.com +from-co.net +from-ct.com +from-dc.com +from-de.com +from-fl.com +from-ga.com +from-hi.com +from-ia.com +from-id.com +from-il.com +from-in.com +from-ks.com +from-ky.com +from-la.net +from-ma.com +from-md.com +from-me.org +from-mi.com +from-mn.com +from-mo.com +from-ms.com +from-mt.com +from-nc.com +from-nd.com +from-ne.com +from-nh.com +from-nj.com +from-nm.com +from-nv.com +from-ny.net +from-oh.com +from-ok.com +from-or.com +from-pa.com +from-pr.com +from-ri.com +from-sc.com +from-sd.com +from-tn.com +from-tx.com +from-ut.com +from-va.com +from-vt.com +from-wa.com +from-wi.com +from-wv.com +from-wy.com +ftpaccess.cc +fuettertdasnetz.de +game-host.org +game-server.cc +getmyip.com +gets-it.net +go.dyndns.org +gotdns.com +gotdns.org +groks-the.info +groks-this.info +ham-radio-op.net +here-for-more.info +hobby-site.com +hobby-site.org +home.dyndns.org +homedns.org +homeftp.net +homeftp.org +homeip.net +homelinux.com +homelinux.net +homelinux.org +homeunix.com +homeunix.net +homeunix.org +iamallama.com +in-the-band.net +is-a-anarchist.com +is-a-blogger.com +is-a-bookkeeper.com +is-a-bruinsfan.org +is-a-bulls-fan.com +is-a-candidate.org +is-a-caterer.com +is-a-celticsfan.org +is-a-chef.com +is-a-chef.net +is-a-chef.org +is-a-conservative.com +is-a-cpa.com +is-a-cubicle-slave.com +is-a-democrat.com +is-a-designer.com +is-a-doctor.com +is-a-financialadvisor.com +is-a-geek.com +is-a-geek.net +is-a-geek.org +is-a-green.com +is-a-guru.com +is-a-hard-worker.com +is-a-hunter.com +is-a-knight.org +is-a-landscaper.com +is-a-lawyer.com +is-a-liberal.com +is-a-libertarian.com +is-a-linux-user.org +is-a-llama.com +is-a-musician.com +is-a-nascarfan.com +is-a-nurse.com +is-a-painter.com +is-a-patsfan.org +is-a-personaltrainer.com +is-a-photographer.com +is-a-player.com +is-a-republican.com +is-a-rockstar.com +is-a-socialist.com +is-a-soxfan.org +is-a-student.com +is-a-teacher.com +is-a-techie.com +is-a-therapist.com +is-an-accountant.com +is-an-actor.com +is-an-actress.com +is-an-anarchist.com +is-an-artist.com +is-an-engineer.com +is-an-entertainer.com +is-by.us +is-certified.com +is-found.org +is-gone.com +is-into-anime.com +is-into-cars.com +is-into-cartoons.com +is-into-games.com +is-leet.com +is-lost.org +is-not-certified.com +is-saved.org +is-slick.com +is-uberleet.com +is-very-bad.org +is-very-evil.org +is-very-good.org +is-very-nice.org +is-very-sweet.org +is-with-theband.com +isa-geek.com +isa-geek.net +isa-geek.org +isa-hockeynut.com +issmarterthanyou.com +isteingeek.de +istmein.de +kicks-ass.net +kicks-ass.org +knowsitall.info +land-4-sale.us +lebtimnetz.de +leitungsen.de +likes-pie.com +likescandy.com +merseine.nu +mine.nu +misconfused.org +mypets.ws +myphotos.cc +neat-url.com +office-on-the.net +on-the-web.tv +podzone.net +podzone.org +readmyblog.org +saves-the-whales.com +scrapper-site.net +scrapping.cc +selfip.biz +selfip.com +selfip.info +selfip.net +selfip.org +sells-for-less.com +sells-for-u.com +sells-it.net +sellsyourhome.org +servebbs.com +servebbs.net +servebbs.org +serveftp.net +serveftp.org +servegame.org +shacknet.nu +simple-url.com +space-to-rent.com +stuff-4-sale.org +stuff-4-sale.us +teaches-yoga.com +thruhere.net +traeumtgerade.de +webhop.biz +webhop.info +webhop.net +webhop.org +worse-than.tv +writesthisblog.com + +// ddnss.de : https://www.ddnss.de/ +// Submitted by Robert Niedziela +ddnss.de +dyn.ddnss.de +dyndns.ddnss.de +dyndns1.de +dyn-ip24.de +home-webserver.de +dyn.home-webserver.de +myhome-server.de +ddnss.org + +// Definima : http://www.definima.com/ +// Submitted by Maxence Bitterli +definima.net +definima.io + +// dnstrace.pro : https://dnstrace.pro/ +// Submitted by Chris Partridge +bci.dnstrace.pro + +// Dynu.com : https://www.dynu.com/ +// Submitted by Sue Ye +ddnsfree.com +ddnsgeek.com +giize.com +gleeze.com +kozow.com +loseyourip.com +ooguy.com +theworkpc.com +casacam.net +dynu.net +accesscam.org +camdvr.org +freeddns.org +mywire.org +webredirect.org +myddns.rocks +blogsite.xyz + +// dynv6 : https://dynv6.com +// Submitted by Dominik Menke +dynv6.net + +// E4YOU spol. s.r.o. : https://e4you.cz/ +// Submitted by Vladimir Dudr +e4.cz + +// Enalean SAS: https://www.enalean.com +// Submitted by Thomas Cottier +mytuleap.com + +// Enonic : http://enonic.com/ +// Submitted by Erik Kaareng-Sunde +enonic.io +customer.enonic.io + +// EU.org https://eu.org/ +// Submitted by Pierre Beyssac +eu.org +al.eu.org +asso.eu.org +at.eu.org +au.eu.org +be.eu.org +bg.eu.org +ca.eu.org +cd.eu.org +ch.eu.org +cn.eu.org +cy.eu.org +cz.eu.org +de.eu.org +dk.eu.org +edu.eu.org +ee.eu.org +es.eu.org +fi.eu.org +fr.eu.org +gr.eu.org +hr.eu.org +hu.eu.org +ie.eu.org +il.eu.org +in.eu.org +int.eu.org +is.eu.org +it.eu.org +jp.eu.org +kr.eu.org +lt.eu.org +lu.eu.org +lv.eu.org +mc.eu.org +me.eu.org +mk.eu.org +mt.eu.org +my.eu.org +net.eu.org +ng.eu.org +nl.eu.org +no.eu.org +nz.eu.org +paris.eu.org +pl.eu.org +pt.eu.org +q-a.eu.org +ro.eu.org +ru.eu.org +se.eu.org +si.eu.org +sk.eu.org +tr.eu.org +uk.eu.org +us.eu.org + +// Evennode : http://www.evennode.com/ +// Submitted by Michal Kralik +eu-1.evennode.com +eu-2.evennode.com +eu-3.evennode.com +eu-4.evennode.com +us-1.evennode.com +us-2.evennode.com +us-3.evennode.com +us-4.evennode.com + +// eDirect Corp. : https://hosting.url.com.tw/ +// Submitted by C.S. chang +twmail.cc +twmail.net +twmail.org +mymailer.com.tw +url.tw + +// Facebook, Inc. +// Submitted by Peter Ruibal +apps.fbsbx.com + +// FAITID : https://faitid.org/ +// Submitted by Maxim Alzoba +// https://www.flexireg.net/stat_info +ru.net +adygeya.ru +bashkiria.ru +bir.ru +cbg.ru +com.ru +dagestan.ru +grozny.ru +kalmykia.ru +kustanai.ru +marine.ru +mordovia.ru +msk.ru +mytis.ru +nalchik.ru +nov.ru +pyatigorsk.ru +spb.ru +vladikavkaz.ru +vladimir.ru +abkhazia.su +adygeya.su +aktyubinsk.su +arkhangelsk.su +armenia.su +ashgabad.su +azerbaijan.su +balashov.su +bashkiria.su +bryansk.su +bukhara.su +chimkent.su +dagestan.su +east-kazakhstan.su +exnet.su +georgia.su +grozny.su +ivanovo.su +jambyl.su +kalmykia.su +kaluga.su +karacol.su +karaganda.su +karelia.su +khakassia.su +krasnodar.su +kurgan.su +kustanai.su +lenug.su +mangyshlak.su +mordovia.su +msk.su +murmansk.su +nalchik.su +navoi.su +north-kazakhstan.su +nov.su +obninsk.su +penza.su +pokrovsk.su +sochi.su +spb.su +tashkent.su +termez.su +togliatti.su +troitsk.su +tselinograd.su +tula.su +tuva.su +vladikavkaz.su +vladimir.su +vologda.su + +// Fancy Bits, LLC : http://getchannels.com +// Submitted by Aman Gupta +channelsdvr.net + +// Fastly Inc. : http://www.fastly.com/ +// Submitted by Fastly Security +fastlylb.net +map.fastlylb.net +freetls.fastly.net +map.fastly.net +a.prod.fastly.net +global.prod.fastly.net +a.ssl.fastly.net +b.ssl.fastly.net +global.ssl.fastly.net + +// FASTVPS EESTI OU : https://fastvps.ru/ +// Submitted by Likhachev Vasiliy +fastpanel.direct +fastvps-server.com + +// Featherhead : https://featherhead.xyz/ +// Submitted by Simon Menke +fhapp.xyz + +// Fedora : https://fedoraproject.org/ +// submitted by Patrick Uiterwijk +fedorainfracloud.org +fedorapeople.org +cloud.fedoraproject.org +app.os.fedoraproject.org +app.os.stg.fedoraproject.org + +// Filegear Inc. : https://www.filegear.com +// Submitted by Jason Zhu +filegear.me + +// Firebase, Inc. +// Submitted by Chris Raynor +firebaseapp.com + +// Flynn : https://flynn.io +// Submitted by Jonathan Rudenberg +flynnhub.com +flynnhosting.net + +// Freebox : http://www.freebox.fr +// Submitted by Romain Fliedel +freebox-os.com +freeboxos.com +fbx-os.fr +fbxos.fr +freebox-os.fr +freeboxos.fr + +// freedesktop.org : https://www.freedesktop.org +// Submitted by Daniel Stone +freedesktop.org + +// Futureweb OG : http://www.futureweb.at +// Submitted by Andreas Schnederle-Wagner +*.futurecms.at +*.ex.futurecms.at +*.in.futurecms.at +futurehosting.at +futuremailing.at +*.ex.ortsinfo.at +*.kunden.ortsinfo.at +*.statics.cloud + +// GDS : https://www.gov.uk/service-manual/operations/operating-servicegovuk-subdomains +// Submitted by David Illsley +service.gov.uk + +// GitHub, Inc. +// Submitted by Patrick Toomey +github.io +githubusercontent.com + +// GitLab, Inc. +// Submitted by Alex Hanselka +gitlab.io + +// UKHomeOffice : https://www.gov.uk/government/organisations/home-office +// Submitted by Jon Shanks +homeoffice.gov.uk + +// GlobeHosting, Inc. +// Submitted by Zoltan Egresi +ro.im +shop.ro + +// GoIP DNS Services : http://www.goip.de +// Submitted by Christian Poulter +goip.de + +// Google, Inc. +// Submitted by Eduardo Vela +*.0emm.com +appspot.com +blogspot.ae +blogspot.al +blogspot.am +blogspot.ba +blogspot.be +blogspot.bg +blogspot.bj +blogspot.ca +blogspot.cf +blogspot.ch +blogspot.cl +blogspot.co.at +blogspot.co.id +blogspot.co.il +blogspot.co.ke +blogspot.co.nz +blogspot.co.uk +blogspot.co.za +blogspot.com +blogspot.com.ar +blogspot.com.au +blogspot.com.br +blogspot.com.by +blogspot.com.co +blogspot.com.cy +blogspot.com.ee +blogspot.com.eg +blogspot.com.es +blogspot.com.mt +blogspot.com.ng +blogspot.com.tr +blogspot.com.uy +blogspot.cv +blogspot.cz +blogspot.de +blogspot.dk +blogspot.fi +blogspot.fr +blogspot.gr +blogspot.hk +blogspot.hr +blogspot.hu +blogspot.ie +blogspot.in +blogspot.is +blogspot.it +blogspot.jp +blogspot.kr +blogspot.li +blogspot.lt +blogspot.lu +blogspot.md +blogspot.mk +blogspot.mr +blogspot.mx +blogspot.my +blogspot.nl +blogspot.no +blogspot.pe +blogspot.pt +blogspot.qa +blogspot.re +blogspot.ro +blogspot.rs +blogspot.ru +blogspot.se +blogspot.sg +blogspot.si +blogspot.sk +blogspot.sn +blogspot.td +blogspot.tw +blogspot.ug +blogspot.vn +cloudfunctions.net +cloud.goog +codespot.com +googleapis.com +googlecode.com +pagespeedmobilizer.com +publishproxy.com +withgoogle.com +withyoutube.com + +// Hashbang : https://hashbang.sh +hashbang.sh + +// Hasura : https://hasura.io +// Submitted by Shahidh K Muhammed +hasura.app +hasura-app.io + +// Hepforge : https://www.hepforge.org +// Submitted by David Grellscheid +hepforge.org + +// Heroku : https://www.heroku.com/ +// Submitted by Tom Maher +herokuapp.com +herokussl.com + +// Hibernating Rhinos +// Submitted by Oren Eini +myravendb.com +ravendb.community +ravendb.me +development.run +ravendb.run + +// Ici la Lune : http://www.icilalune.com/ +// Submitted by Simon Morvan +moonscale.net + +// iki.fi +// Submitted by Hannu Aronsson +iki.fi + +// info.at : http://www.info.at/ +biz.at +info.at + +// info.cx : http://info.cx +// Submitted by Jacob Slater +info.cx + +// Interlegis : http://www.interlegis.leg.br +// Submitted by Gabriel Ferreira +ac.leg.br +al.leg.br +am.leg.br +ap.leg.br +ba.leg.br +ce.leg.br +df.leg.br +es.leg.br +go.leg.br +ma.leg.br +mg.leg.br +ms.leg.br +mt.leg.br +pa.leg.br +pb.leg.br +pe.leg.br +pi.leg.br +pr.leg.br +rj.leg.br +rn.leg.br +ro.leg.br +rr.leg.br +rs.leg.br +sc.leg.br +se.leg.br +sp.leg.br +to.leg.br + +// intermetrics GmbH : https://pixolino.com/ +// Submitted by Wolfgang Schwarz +pixolino.com + +// IPiFony Systems, Inc. : https://www.ipifony.com/ +// Submitted by Matthew Hardeman +ipifony.net + +// IServ GmbH : https://iserv.eu +// Submitted by Kim-Alexander Brodowski +mein-iserv.de +test-iserv.de + +// Jino : https://www.jino.ru +// Submitted by Sergey Ulyashin +myjino.ru +*.hosting.myjino.ru +*.landing.myjino.ru +*.spectrum.myjino.ru +*.vps.myjino.ru + +// Joyent : https://www.joyent.com/ +// Submitted by Brian Bennett +*.triton.zone +*.cns.joyent.com + +// JS.ORG : http://dns.js.org +// Submitted by Stefan Keim +js.org + +// Keyweb AG : https://www.keyweb.de +// Submitted by Martin Dannehl +keymachine.de + +// KnightPoint Systems, LLC : http://www.knightpoint.com/ +// Submitted by Roy Keene +knightpoint.systems + +// .KRD : http://nic.krd/data/krd/Registration%20Policy.pdf +co.krd +edu.krd + +// LCube - Professional hosting e.K. : https://www.lcube-webhosting.de +// Submitted by Lars Laehn +git-repos.de +lcube-server.de +svn-repos.de + +// Lightmaker Property Manager, Inc. : https://app.lmpm.com/ +// Submitted by Greg Holland +app.lmpm.com + +// Linki Tools UG : https://linki.tools +// Submitted by Paulo Matos +linkitools.space + +// linkyard ldt: https://www.linkyard.ch/ +// Submitted by Mario Siegenthaler +linkyard.cloud +linkyard-cloud.ch + +// LiquidNet Ltd : http://www.liquidnetlimited.com/ +// Submitted by Victor Velchev +we.bs + +// Lug.org.uk : https://lug.org.uk +// Submitted by Jon Spriggs +uklugs.org +glug.org.uk +lug.org.uk +lugs.org.uk + +// Lukanet Ltd : https://lukanet.com +// Submitted by Anton Avramov +barsy.bg +barsy.co.uk +barsyonline.co.uk +barsycenter.com +barsyonline.com +barsy.club +barsy.de +barsy.eu +barsy.in +barsy.info +barsy.io +barsy.me +barsy.menu +barsy.mobi +barsy.net +barsy.online +barsy.org +barsy.pro +barsy.pub +barsy.shop +barsy.site +barsy.support +barsy.uk + +// Magento Commerce +// Submitted by Damien Tournoud +*.magentosite.cloud + +// May First - People Link : https://mayfirst.org/ +// Submitted by Jamie McClelland +mayfirst.info +mayfirst.org + +// Mail.Ru Group : https://hb.cldmail.ru +// Submitted by Ilya Zaretskiy +hb.cldmail.ru + +// Memset hosting : https://www.memset.com +// Submitted by Tom Whitwell +miniserver.com +memset.net + +// MetaCentrum, CESNET z.s.p.o. : https://www.metacentrum.cz/en/ +// Submitted by Zdeněk Šustr +cloud.metacentrum.cz +custom.metacentrum.cz + +// MetaCentrum, CESNET z.s.p.o. : https://www.metacentrum.cz/en/ +// Submitted by Radim Janča +flt.cloud.muni.cz +usr.cloud.muni.cz + +// Meteor Development Group : https://www.meteor.com/hosting +// Submitted by Pierre Carrier +meteorapp.com +eu.meteorapp.com + +// Michau Enterprises Limited : http://www.co.pl/ +co.pl + +// Microsoft Corporation : http://microsoft.com +// Submitted by Justin Luk +azurecontainer.io +azurewebsites.net +azure-mobile.net +cloudapp.net + +// Mozilla Corporation : https://mozilla.com +// Submitted by Ben Francis +mozilla-iot.org + +// Mozilla Foundation : https://mozilla.org/ +// Submitted by glob +bmoattachments.org + +// MSK-IX : https://www.msk-ix.ru/ +// Submitted by Khannanov Roman +net.ru +org.ru +pp.ru + +// Netlify : https://www.netlify.com +// Submitted by Jessica Parsons +bitballoon.com +netlify.com + +// Neustar Inc. +// Submitted by Trung Tran +4u.com + +// ngrok : https://ngrok.com/ +// Submitted by Alan Shreve +ngrok.io + +// Nimbus Hosting Ltd. : https://www.nimbushosting.co.uk/ +// Submitted by Nicholas Ford +nh-serv.co.uk + +// NFSN, Inc. : https://www.NearlyFreeSpeech.NET/ +// Submitted by Jeff Wheelhouse +nfshost.com + +// Now-DNS : https://now-dns.com +// Submitted by Steve Russell +dnsking.ch +mypi.co +n4t.co +001www.com +ddnslive.com +myiphost.com +forumz.info +16-b.it +32-b.it +64-b.it +soundcast.me +tcp4.me +dnsup.net +hicam.net +now-dns.net +ownip.net +vpndns.net +dynserv.org +now-dns.org +x443.pw +now-dns.top +ntdll.top +freeddns.us +crafting.xyz +zapto.xyz + +// nsupdate.info : https://www.nsupdate.info/ +// Submitted by Thomas Waldmann +nsupdate.info +nerdpol.ovh + +// No-IP.com : https://noip.com/ +// Submitted by Deven Reza +blogsyte.com +brasilia.me +cable-modem.org +ciscofreak.com +collegefan.org +couchpotatofries.org +damnserver.com +ddns.me +ditchyourip.com +dnsfor.me +dnsiskinky.com +dvrcam.info +dynns.com +eating-organic.net +fantasyleague.cc +geekgalaxy.com +golffan.us +health-carereform.com +homesecuritymac.com +homesecuritypc.com +hopto.me +ilovecollege.info +loginto.me +mlbfan.org +mmafan.biz +myactivedirectory.com +mydissent.net +myeffect.net +mymediapc.net +mypsx.net +mysecuritycamera.com +mysecuritycamera.net +mysecuritycamera.org +net-freaks.com +nflfan.org +nhlfan.net +no-ip.ca +no-ip.co.uk +no-ip.net +noip.us +onthewifi.com +pgafan.net +point2this.com +pointto.us +privatizehealthinsurance.net +quicksytes.com +read-books.org +securitytactics.com +serveexchange.com +servehumour.com +servep2p.com +servesarcasm.com +stufftoread.com +ufcfan.org +unusualperson.com +workisboring.com +3utilities.com +bounceme.net +ddns.net +ddnsking.com +gotdns.ch +hopto.org +myftp.biz +myftp.org +myvnc.com +no-ip.biz +no-ip.info +no-ip.org +noip.me +redirectme.net +servebeer.com +serveblog.net +servecounterstrike.com +serveftp.com +servegame.com +servehalflife.com +servehttp.com +serveirc.com +serveminecraft.net +servemp3.com +servepics.com +servequake.com +sytes.net +webhop.me +zapto.org + +// NodeArt : https://nodeart.io +// Submitted by Konstantin Nosov +stage.nodeart.io + +// Nodum B.V. : https://nodum.io/ +// Submitted by Wietse Wind +nodum.co +nodum.io + +// Nucleos Inc. : https://nucleos.com +// Submitted by Piotr Zduniak +pcloud.host + +// NYC.mn : http://www.information.nyc.mn +// Submitted by Matthew Brown +nyc.mn + +// NymNom : https://nymnom.com/ +// Submitted by Dave McCormack +nom.ae +nom.af +nom.ai +nom.al +nym.by +nym.bz +nom.cl +nom.gd +nom.ge +nom.gl +nym.gr +nom.gt +nym.gy +nom.hn +nym.ie +nom.im +nom.ke +nym.kz +nym.la +nym.lc +nom.li +nym.li +nym.lt +nym.lu +nym.me +nom.mk +nym.mn +nym.mx +nom.nu +nym.nz +nym.pe +nym.pt +nom.pw +nom.qa +nym.ro +nom.rs +nom.si +nym.sk +nom.st +nym.su +nym.sx +nom.tj +nym.tw +nom.ug +nom.uy +nom.vc +nom.vg + +// Octopodal Solutions, LLC. : https://ulterius.io/ +// Submitted by Andrew Sampson +cya.gg + +// Omnibond Systems, LLC. : https://www.omnibond.com +// Submitted by Cole Estep +cloudycluster.net + +// One Fold Media : http://www.onefoldmedia.com/ +// Submitted by Eddie Jones +nid.io + +// OpenCraft GmbH : http://opencraft.com/ +// Submitted by Sven Marnach +opencraft.hosting + +// Opera Software, A.S.A. +// Submitted by Yngve Pettersen +operaunite.com + +// OutSystems +// Submitted by Duarte Santos +outsystemscloud.com + +// OwnProvider GmbH: http://www.ownprovider.com +// Submitted by Jan Moennich +ownprovider.com +own.pm + +// OX : http://www.ox.rs +// Submitted by Adam Grand +ox.rs + +// oy.lc +// Submitted by Charly Coste +oy.lc + +// Pagefog : https://pagefog.com/ +// Submitted by Derek Myers +pgfog.com + +// Pagefront : https://www.pagefronthq.com/ +// Submitted by Jason Kriss +pagefrontapp.com + +// .pl domains (grandfathered) +art.pl +gliwice.pl +krakow.pl +poznan.pl +wroc.pl +zakopane.pl + +// Pantheon Systems, Inc. : https://pantheon.io/ +// Submitted by Gary Dylina +pantheonsite.io +gotpantheon.com + +// Peplink | Pepwave : http://peplink.com/ +// Submitted by Steve Leung +mypep.link + +// Planet-Work : https://www.planet-work.com/ +// Submitted by Frédéric VANNIÈRE +on-web.fr + +// Platform.sh : https://platform.sh +// Submitted by Nikola Kotur +*.platform.sh +*.platformsh.site + +// prgmr.com : https://prgmr.com/ +// Submitted by Sarah Newman +xen.prgmr.com + +// priv.at : http://www.nic.priv.at/ +// Submitted by registry +priv.at + +// Protonet GmbH : http://protonet.io +// Submitted by Martin Meier +protonet.io + +// Publication Presse Communication SARL : https://ppcom.fr +// Submitted by Yaacov Akiba Slama +chirurgiens-dentistes-en-france.fr +byen.site + +// Russian Academy of Sciences +// Submitted by Tech Support +ras.ru + +// QA2 +// Submitted by Daniel Dent (https://www.danieldent.com/) +qa2.com + +// QNAP System Inc : https://www.qnap.com +// Submitted by Nick Chang +dev-myqnapcloud.com +alpha-myqnapcloud.com +myqnapcloud.com + +// Quip : https://quip.com +// Submitted by Patrick Linehan +*.quipelements.com + +// Qutheory LLC : http://qutheory.io +// Submitted by Jonas Schwartz +vapor.cloud +vaporcloud.io + +// Rackmaze LLC : https://www.rackmaze.com +// Submitted by Kirill Pertsev +rackmaze.com +rackmaze.net + +// Red Hat, Inc. OpenShift : https://openshift.redhat.com/ +// Submitted by Tim Kramer +rhcloud.com + +// Resin.io : https://resin.io +// Submitted by Tim Perry +resindevice.io +devices.resinstaging.io + +// RethinkDB : https://www.rethinkdb.com/ +// Submitted by Chris Kastorff +hzc.io + +// Revitalised Limited : http://www.revitalised.co.uk +// Submitted by Jack Price +wellbeingzone.eu +ptplus.fit +wellbeingzone.co.uk + +// Sandstorm Development Group, Inc. : https://sandcats.io/ +// Submitted by Asheesh Laroia +sandcats.io + +// SBE network solutions GmbH : https://www.sbe.de/ +// Submitted by Norman Meilick +logoip.de +logoip.com + +// schokokeks.org GbR : https://schokokeks.org/ +// Submitted by Hanno Böck +schokokeks.net + +// Scry Security : http://www.scrysec.com +// Submitted by Shante Adam +scrysec.com + +// Securepoint GmbH : https://www.securepoint.de +// Submitted by Erik Anders +firewall-gateway.com +firewall-gateway.de +my-gateway.de +my-router.de +spdns.de +spdns.eu +firewall-gateway.net +my-firewall.org +myfirewall.org +spdns.org + +// SensioLabs, SAS : https://sensiolabs.com/ +// Submitted by Fabien Potencier +*.s5y.io +*.sensiosite.cloud + +// Service Online LLC : http://drs.ua/ +// Submitted by Serhii Bulakh +biz.ua +co.ua +pp.ua + +// ShiftEdit : https://shiftedit.net/ +// Submitted by Adam Jimenez +shiftedit.io + +// Shopblocks : http://www.shopblocks.com/ +// Submitted by Alex Bowers +myshopblocks.com + +// SinaAppEngine : http://sae.sina.com.cn/ +// Submitted by SinaAppEngine +1kapp.com +appchizi.com +applinzi.com +sinaapp.com +vipsinaapp.com + +// Skyhat : http://www.skyhat.io +// Submitted by Shante Adam +bounty-full.com +alpha.bounty-full.com +beta.bounty-full.com + +// staticland : https://static.land +// Submitted by Seth Vincent +static.land +dev.static.land +sites.static.land + +// SourceLair PC : https://www.sourcelair.com +// Submitted by Antonis Kalipetis +apps.lair.io +*.stolos.io + +// SpaceKit : https://www.spacekit.io/ +// Submitted by Reza Akhavan +spacekit.io + +// SpeedPartner GmbH: https://www.speedpartner.de/ +// Submitted by Stefan Neufeind +customer.speedpartner.de + +// Storj Labs Inc. : https://storj.io/ +// Submitted by Philip Hutchins +storj.farm + +// Studenten Net Twente : http://www.snt.utwente.nl/ +// Submitted by Silke Hofstra +utwente.io + +// Sub 6 Limited: http://www.sub6.com +// Submitted by Dan Miller +temp-dns.com + +// Synology, Inc. : https://www.synology.com/ +// Submitted by Rony Weng +diskstation.me +dscloud.biz +dscloud.me +dscloud.mobi +dsmynas.com +dsmynas.net +dsmynas.org +familyds.com +familyds.net +familyds.org +i234.me +myds.me +synology.me +vpnplus.to + +// TAIFUN Software AG : http://taifun-software.de +// Submitted by Bjoern Henke +taifun-dns.de + +// TASK geographical domains (www.task.gda.pl/uslugi/dns) +gda.pl +gdansk.pl +gdynia.pl +med.pl +sopot.pl + +// The Gwiddle Foundation : https://gwiddlefoundation.org.uk +// Submitted by Joshua Bayfield +gwiddle.co.uk + +// Thingdust AG : https://thingdust.com/ +// Submitted by Adrian Imboden +cust.dev.thingdust.io +cust.disrec.thingdust.io +cust.prod.thingdust.io +cust.testing.thingdust.io + +// TownNews.com : http://www.townnews.com +// Submitted by Dustin Ward +bloxcms.com +townnews-staging.com + +// TrafficPlex GmbH : https://www.trafficplex.de/ +// Submitted by Phillipp Röll +12hp.at +2ix.at +4lima.at +lima-city.at +12hp.ch +2ix.ch +4lima.ch +lima-city.ch +trafficplex.cloud +de.cool +12hp.de +2ix.de +4lima.de +lima-city.de +1337.pictures +clan.rip +lima-city.rocks +webspace.rocks +lima.zone + +// TransIP : https://www.transip.nl +// Submitted by Rory Breuk +*.transurl.be +*.transurl.eu +*.transurl.nl + +// TuxFamily : http://tuxfamily.org +// Submitted by TuxFamily administrators +tuxfamily.org + +// TwoDNS : https://www.twodns.de/ +// Submitted by TwoDNS-Support +dd-dns.de +diskstation.eu +diskstation.org +dray-dns.de +draydns.de +dyn-vpn.de +dynvpn.de +mein-vigor.de +my-vigor.de +my-wan.de +syno-ds.de +synology-diskstation.de +synology-ds.de + +// Uberspace : https://uberspace.de +// Submitted by Moritz Werner +uber.space +*.uberspace.de + +// UDR Limited : http://www.udr.hk.com +// Submitted by registry +hk.com +hk.org +ltd.hk +inc.hk + +// United Gameserver GmbH : https://united-gameserver.de +// Submitted by Stefan Schwarz +virtualuser.de +virtual-user.de + +// .US +// Submitted by Ed Moore +lib.de.us + +// VeryPositive SIA : http://very.lv +// Submitted by Danko Aleksejevs +2038.io + +// Viprinet Europe GmbH : http://www.viprinet.com +// Submitted by Simon Kissel +router.management + +// Virtual-Info : https://www.virtual-info.info/ +// Submitted by Adnan RIHAN +v-info.info + +// WeDeploy by Liferay, Inc. : https://www.wedeploy.com +// Submitted by Henrique Vicente +wedeploy.io +wedeploy.me +wedeploy.sh + +// Western Digital Technologies, Inc : https://www.wdc.com +// Submitted by Jung Jin +remotewd.com + +// Wikimedia Labs : https://wikitech.wikimedia.org +// Submitted by Yuvi Panda +wmflabs.org + +// XenonCloud GbR: https://xenoncloud.net +// Submitted by Julian Uphoff +half.host + +// XnBay Technology : http://www.xnbay.com/ +// Submitted by XnBay Developer +xnbay.com +u2.xnbay.com +u2-local.xnbay.com + +// XS4ALL Internet bv : https://www.xs4all.nl/ +// Submitted by Daniel Mostertman +cistron.nl +demon.nl +xs4all.space + +// YesCourse Pty Ltd : https://yescourse.com +// Submitted by Atul Bhouraskar +official.academy + +// Yola : https://www.yola.com/ +// Submitted by Stefano Rivera +yolasite.com + +// Yombo : https://yombo.net +// Submitted by Mitch Schwenk +ybo.faith +yombo.me +homelink.one +ybo.party +ybo.review +ybo.science +ybo.trade + +// Yunohost : https://yunohost.org +// Submitted by Valentin Grimaud +nohost.me +noho.st + +// ZaNiC : http://www.za.net/ +// Submitted by registry +za.net +za.org + +// Zeit, Inc. : https://zeit.domains/ +// Submitted by Olli Vanhoja +now.sh + +// Zone.id : https://zone.id/ +// Submitted by Su Hendro +zone.id + +// ===END PRIVATE DOMAINS=== diff --git a/src/Maui/Bitwarden/Core/Services/ApiService.cs b/src/Maui/Bitwarden/Core/Services/ApiService.cs new file mode 100644 index 000000000..6b2dbe48e --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/ApiService.cs @@ -0,0 +1,886 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; +using Bit.Core.Models.Response; +using Bit.Core.Utilities; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; + +namespace Bit.Core.Services +{ + public class ApiService : IApiService + { + private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + private readonly HttpClient _httpClient = new HttpClient(); + private readonly ITokenService _tokenService; + private readonly IPlatformUtilsService _platformUtilsService; + private readonly Func, Task> _logoutCallbackAsync; + + public ApiService( + ITokenService tokenService, + IPlatformUtilsService platformUtilsService, + Func, Task> logoutCallbackAsync, + string customUserAgent = null) + { + _tokenService = tokenService; + _platformUtilsService = platformUtilsService; + _logoutCallbackAsync = logoutCallbackAsync; + var device = (int)_platformUtilsService.GetDevice(); + _httpClient.DefaultRequestHeaders.Add("Device-Type", device.ToString()); + _httpClient.DefaultRequestHeaders.Add("Bitwarden-Client-Name", _platformUtilsService.GetClientType().GetString()); + _httpClient.DefaultRequestHeaders.Add("Bitwarden-Client-Version", _platformUtilsService.GetApplicationVersion()); + if (!string.IsNullOrWhiteSpace(customUserAgent)) + { + _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(customUserAgent); + } + } + + public bool UrlsSet { get; private set; } + public string ApiBaseUrl { get; set; } + public string IdentityBaseUrl { get; set; } + public string EventsBaseUrl { get; set; } + + public void SetUrls(EnvironmentUrls urls) + { + UrlsSet = true; + if (!string.IsNullOrWhiteSpace(urls.Base)) + { + ApiBaseUrl = urls.Base + "/api"; + IdentityBaseUrl = urls.Base + "/identity"; + EventsBaseUrl = urls.Base + "/events"; + return; + } + + ApiBaseUrl = urls.Api; + IdentityBaseUrl = urls.Identity; + EventsBaseUrl = urls.Events; + + // Production + if (string.IsNullOrWhiteSpace(ApiBaseUrl)) + { + ApiBaseUrl = "https://api.bitwarden.com"; + } + if (string.IsNullOrWhiteSpace(IdentityBaseUrl)) + { + IdentityBaseUrl = "https://identity.bitwarden.com"; + } + if (string.IsNullOrWhiteSpace(EventsBaseUrl)) + { + EventsBaseUrl = "https://events.bitwarden.com"; + } + } + + #region Auth APIs + + public async Task PostIdentityTokenAsync(TokenRequest request) + { + var requestMessage = new HttpRequestMessage + { + Version = new Version(1, 0), + RequestUri = new Uri(string.Concat(IdentityBaseUrl, "/connect/token")), + Method = HttpMethod.Post, + Content = new FormUrlEncodedContent(request.ToIdentityToken(_platformUtilsService.GetClientType().GetString())) + }; + requestMessage.Headers.Add("Accept", "application/json"); + request.AlterIdentityTokenHeaders(requestMessage.Headers); + + HttpResponseMessage response; + try + { + response = await _httpClient.SendAsync(requestMessage); + } + catch (Exception e) + { + throw new ApiException(HandleWebError(e)); + } + JObject responseJObject = null; + if (IsJsonResponse(response)) + { + var responseJsonString = await response.Content.ReadAsStringAsync(); + responseJObject = JObject.Parse(responseJsonString); + } + + var identityResponse = new IdentityResponse(response.StatusCode, responseJObject); + + if (identityResponse.FailedToParse) + { + throw new ApiException(new ErrorResponse(responseJObject, response.StatusCode, true)); + } + + return identityResponse; + } + + public async Task RefreshIdentityTokenAsync() + { + try + { + await DoRefreshTokenAsync(); + } + catch + { + throw new ApiException(); + } + } + + #endregion + + #region Account APIs + + public Task GetProfileAsync() + { + return SendAsync(HttpMethod.Get, "/accounts/profile", null, true, true); + } + + public Task PostPreloginAsync(PreloginRequest request) + { + return SendAsync(HttpMethod.Post, "/accounts/prelogin", + request, false, true); + } + + public Task GetAccountRevisionDateAsync() + { + return SendAsync(HttpMethod.Get, "/accounts/revision-date", null, true, true); + } + + public Task PostPasswordHintAsync(PasswordHintRequest request) + { + return SendAsync(HttpMethod.Post, "/accounts/password-hint", + request, false, false); + } + + public Task SetPasswordAsync(SetPasswordRequest request) + { + return SendAsync(HttpMethod.Post, "/accounts/set-password", request, true, + false); + } + + public Task PostRegisterAsync(RegisterRequest request) + { + return SendAsync(HttpMethod.Post, "/accounts/register", request, false, false); + } + + public Task PostAccountKeysAsync(KeysRequest request) + { + return SendAsync(HttpMethod.Post, "/accounts/keys", request, true, false); + } + + public Task PostAccountVerifyPasswordAsync(PasswordVerificationRequest request) + { + return SendAsync(HttpMethod.Post, "/accounts/verify-password", request, + true, true); + } + + public Task PostAccountRequestOTP() + { + return SendAsync(HttpMethod.Post, "/accounts/request-otp", null, true, false, null, false); + } + + public Task PostAccountVerifyOTPAsync(VerifyOTPRequest request) + { + return SendAsync(HttpMethod.Post, "/accounts/verify-otp", request, + true, false, null, false); + } + + public Task PutUpdateTempPasswordAsync(UpdateTempPasswordRequest request) + { + return SendAsync(HttpMethod.Put, "/accounts/update-temp-password", + request, true, false); + } + + public Task PostPasswordAsync(PasswordRequest request) + { + return SendAsync(HttpMethod.Post, "/accounts/password", request, true, false); + } + + public Task DeleteAccountAsync(DeleteAccountRequest request) + { + return SendAsync(HttpMethod.Delete, "/accounts", request, true, false); + } + + public Task PostConvertToKeyConnector() + { + return SendAsync(HttpMethod.Post, "/accounts/convert-to-key-connector", null, true, false); + } + + public Task PostSetKeyConnectorKey(SetKeyConnectorKeyRequest request) + { + return SendAsync(HttpMethod.Post, "/accounts/set-key-connector-key", request, true); + } + + #endregion + + #region Folder APIs + + public Task GetFolderAsync(string id) + { + return SendAsync(HttpMethod.Get, string.Concat("/folders/", id), + null, true, true); + } + + public Task PostFolderAsync(FolderRequest request) + { + return SendAsync(HttpMethod.Post, "/folders", request, true, true); + } + + public async Task PutFolderAsync(string id, FolderRequest request) + { + return await SendAsync(HttpMethod.Put, string.Concat("/folders/", id), + request, true, true); + } + + public Task DeleteFolderAsync(string id) + { + return SendAsync(HttpMethod.Delete, string.Concat("/folders/", id), null, true, false); + } + + #endregion + + #region Send APIs + + public Task GetSendAsync(string id) => + SendAsync(HttpMethod.Get, $"/sends/{id}", null, true, true); + + public Task PostSendAsync(SendRequest request) => + SendAsync(HttpMethod.Post, "/sends", request, true, true); + + public Task PostFileTypeSendAsync(SendRequest request) => + SendAsync(HttpMethod.Post, "/sends/file/v2", request, true, true); + + public Task PostSendFileAsync(string sendId, string fileId, MultipartFormDataContent data) => + SendAsync(HttpMethod.Post, $"/sends/{sendId}/file/{fileId}", data, true, false); + + [Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")] + public Task PostSendFileAsync(MultipartFormDataContent data) => + SendAsync(HttpMethod.Post, "/sends/file", data, true, true); + + public Task RenewFileUploadUrlAsync(string sendId, string fileId) => + SendAsync(HttpMethod.Get, $"/sends/{sendId}/file/{fileId}", null, true, true); + + public Task PutSendAsync(string id, SendRequest request) => + SendAsync(HttpMethod.Put, $"/sends/{id}", request, true, true); + + public Task PutSendRemovePasswordAsync(string id) => + SendAsync(HttpMethod.Put, $"/sends/{id}/remove-password", null, true, true); + + public Task DeleteSendAsync(string id) => + SendAsync(HttpMethod.Delete, $"/sends/{id}", null, true, false); + + #endregion + + #region Cipher APIs + + public Task GetCipherAsync(string id) + { + return SendAsync(HttpMethod.Get, string.Concat("/ciphers/", id), + null, true, true); + } + + public Task PostCipherAsync(CipherRequest request) + { + return SendAsync(HttpMethod.Post, "/ciphers", request, true, true); + } + + public Task PostCipherCreateAsync(CipherCreateRequest request) + { + return SendAsync(HttpMethod.Post, "/ciphers/create", + request, true, true); + } + + public Task PutCipherAsync(string id, CipherRequest request) + { + return SendAsync(HttpMethod.Put, string.Concat("/ciphers/", id), + request, true, true); + } + + public Task PutShareCipherAsync(string id, CipherShareRequest request) + { + return SendAsync(HttpMethod.Put, + string.Concat("/ciphers/", id, "/share"), request, true, true); + } + + public Task PutCipherCollectionsAsync(string id, CipherCollectionsRequest request) + { + return SendAsync(HttpMethod.Put, + string.Concat("/ciphers/", id, "/collections"), request, true, false); + } + + public Task DeleteCipherAsync(string id) + { + return SendAsync(HttpMethod.Delete, string.Concat("/ciphers/", id), null, true, false); + } + + public Task PutDeleteCipherAsync(string id) + { + return SendAsync(HttpMethod.Put, string.Concat("/ciphers/", id, "/delete"), null, true, false); + } + + public Task PutRestoreCipherAsync(string id) + { + return SendAsync(HttpMethod.Put, string.Concat("/ciphers/", id, "/restore"), null, true, true); + } + + #endregion + + #region Attachments APIs + + [Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")] + public Task PostCipherAttachmentLegacyAsync(string id, MultipartFormDataContent data) + { + return SendAsync(HttpMethod.Post, + string.Concat("/ciphers/", id, "/attachment"), data, true, true); + } + + public Task PostCipherAttachmentAsync(string id, AttachmentRequest request) + { + return SendAsync(HttpMethod.Post, + $"/ciphers/{id}/attachment/v2", request, true, true); + } + + public Task GetAttachmentData(string cipherId, string attachmentId) => + SendAsync(HttpMethod.Get, $"/ciphers/{cipherId}/attachment/{attachmentId}", true); + + public Task DeleteCipherAttachmentAsync(string id, string attachmentId) + { + return SendAsync(HttpMethod.Delete, + string.Concat("/ciphers/", id, "/attachment/", attachmentId), true); + } + + public Task PostShareCipherAttachmentAsync(string id, string attachmentId, MultipartFormDataContent data, + string organizationId) + { + return SendAsync(HttpMethod.Post, + string.Concat("/ciphers/", id, "/attachment/", attachmentId, "/share?organizationId=", organizationId), + data, true, false); + } + + public Task RenewAttachmentUploadUrlAsync(string cipherId, string attachmentId) => + SendAsync(HttpMethod.Get, $"/ciphers/{cipherId}/attachment/{attachmentId}/renew", true); + + public Task PostAttachmentFileAsync(string cipherId, string attachmentId, MultipartFormDataContent data) => + SendAsync(HttpMethod.Post, + $"/ciphers/{cipherId}/attachment/{attachmentId}", data, true); + + #endregion + + #region Sync APIs + + public Task GetSyncAsync() + { + return SendAsync(HttpMethod.Get, "/sync", null, true, true); + } + + #endregion + + #region Two Factor APIs + + public Task PostTwoFactorEmailAsync(TwoFactorEmailRequest request) + { + return SendAsync( + HttpMethod.Post, "/two-factor/send-email-login", request, false, false); + } + + #endregion + + #region Device APIs + + public Task PutDeviceTokenAsync(string identifier, DeviceTokenRequest request) + { + return SendAsync( + HttpMethod.Put, $"/devices/identifier/{identifier}/token", request, true, false); + } + + #endregion + + #region Event APIs + + public async Task PostEventsCollectAsync(IEnumerable request) + { + using (var requestMessage = new HttpRequestMessage()) + { + requestMessage.Version = new Version(1, 0); + requestMessage.Method = HttpMethod.Post; + requestMessage.RequestUri = new Uri(string.Concat(EventsBaseUrl, "/collect")); + var authHeader = await GetActiveBearerTokenAsync(); + requestMessage.Headers.Add("Authorization", string.Concat("Bearer ", authHeader)); + requestMessage.Content = new StringContent(JsonConvert.SerializeObject(request, _jsonSettings), + Encoding.UTF8, "application/json"); + HttpResponseMessage response; + try + { + response = await _httpClient.SendAsync(requestMessage); + } + catch (Exception e) + { + throw new ApiException(HandleWebError(e)); + } + if (!response.IsSuccessStatusCode) + { + var error = await HandleErrorAsync(response, false, false); + throw new ApiException(error); + } + } + } + + #endregion + + #region HIBP APIs + + public Task> GetHibpBreachAsync(string username) + { + return SendAsync>(HttpMethod.Get, + string.Concat("/hibp/breach?username=", username), null, true, true); + } + + #endregion + + #region Organizations APIs + + public Task GetOrganizationKeysAsync(string id) + { + return SendAsync(HttpMethod.Get, $"/organizations/{id}/keys", null, true, true); + } + + public Task GetOrganizationAutoEnrollStatusAsync(string identifier) + { + return SendAsync(HttpMethod.Get, + $"/organizations/{identifier}/auto-enroll-status", null, true, true); + } + + public Task PostLeaveOrganization(string id) + { + return SendAsync(HttpMethod.Post, $"/organizations/{id}/leave", null, true, false); + } + + + public Task GetOrgDomainSsoDetailsAsync(string userEmail) + { + return SendAsync(HttpMethod.Post, $"/organizations/domain/sso/details", new OrganizationDomainSsoDetailsRequest { Email = userEmail }, false, true); + } + #endregion + + #region Organization User APIs + + public Task PutOrganizationUserResetPasswordEnrollmentAsync(string orgId, string userId, + OrganizationUserResetPasswordEnrollmentRequest request) + { + return SendAsync(HttpMethod.Put, + $"/organizations/{orgId}/users/{userId}/reset-password-enrollment", request, true, false); + } + + #endregion + + #region Key Connector + + public async Task GetUserKeyFromKeyConnector(string keyConnectorUrl) + { + using (var requestMessage = new HttpRequestMessage()) + { + var authHeader = await GetActiveBearerTokenAsync(); + + requestMessage.Version = new Version(1, 0); + requestMessage.Method = HttpMethod.Get; + requestMessage.RequestUri = new Uri(string.Concat(keyConnectorUrl, "/user-keys")); + requestMessage.Headers.Add("Authorization", string.Concat("Bearer ", authHeader)); + + HttpResponseMessage response; + try + { + response = await _httpClient.SendAsync(requestMessage); + } + catch (Exception e) + { + throw new ApiException(HandleWebError(e)); + } + if (!response.IsSuccessStatusCode) + { + var error = await HandleErrorAsync(response, false, true); + throw new ApiException(error); + } + var responseJsonString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(responseJsonString); + } + } + + public async Task PostUserKeyToKeyConnector(string keyConnectorUrl, KeyConnectorUserKeyRequest request) + { + using (var requestMessage = new HttpRequestMessage()) + { + var authHeader = await GetActiveBearerTokenAsync(); + + requestMessage.Version = new Version(1, 0); + requestMessage.Method = HttpMethod.Post; + requestMessage.RequestUri = new Uri(string.Concat(keyConnectorUrl, "/user-keys")); + requestMessage.Headers.Add("Authorization", string.Concat("Bearer ", authHeader)); + requestMessage.Content = new StringContent(JsonConvert.SerializeObject(request, _jsonSettings), + Encoding.UTF8, "application/json"); + + HttpResponseMessage response; + try + { + response = await _httpClient.SendAsync(requestMessage); + } + catch (Exception e) + { + throw new ApiException(HandleWebError(e)); + } + if (!response.IsSuccessStatusCode) + { + var error = await HandleErrorAsync(response, false, true); + throw new ApiException(error); + } + } + } + + #endregion + + #region PasswordlessLogin + + public async Task> GetAuthRequestAsync() + { + var response = await SendAsync(HttpMethod.Get, $"/auth-requests/", null, true, true); + return response.Data; + } + + public Task GetAuthRequestAsync(string id) + { + return SendAsync(HttpMethod.Get, $"/auth-requests/{id}", null, true, true); + } + + public Task GetAuthResponseAsync(string id, string accessCode) + { + return SendAsync(HttpMethod.Get, $"/auth-requests/{id}/response?code={accessCode}", null, false, true); + } + + public Task PostCreateRequestAsync(PasswordlessCreateLoginRequest passwordlessCreateLoginRequest) + { + return SendAsync(HttpMethod.Post, $"/auth-requests", passwordlessCreateLoginRequest, false, true); + } + + public Task PutAuthRequestAsync(string id, string encKey, string encMasterPasswordHash, string deviceIdentifier, bool requestApproved) + { + var request = new PasswordlessLoginRequest(encKey, encMasterPasswordHash, deviceIdentifier, requestApproved); + return SendAsync(HttpMethod.Put, $"/auth-requests/{id}", request, true, true); + } + + public Task GetKnownDeviceAsync(string email, string deviceIdentifier) + { + return SendAsync(HttpMethod.Get, "/devices/knowndevice", null, false, true, (message) => + { + message.Headers.Add("X-Device-Identifier", deviceIdentifier); + message.Headers.Add("X-Request-Email", CoreHelpers.Base64UrlEncode(Encoding.UTF8.GetBytes(email))); + }); + } + + #endregion + + #region Configs + + public async Task GetConfigsAsync() + { + var accessToken = await _tokenService.GetTokenAsync(); + return await SendAsync(HttpMethod.Get, "/config/", null, !string.IsNullOrEmpty(accessToken), true); + } + + #endregion + + #region Helpers + + public async Task GetActiveBearerTokenAsync() + { + var accessToken = await _tokenService.GetTokenAsync(); + if (_tokenService.TokenNeedsRefresh()) + { + var tokenResponse = await DoRefreshTokenAsync(); + accessToken = tokenResponse.AccessToken; + } + return accessToken; + } + + public async Task PreValidateSso(string identifier) + { + var path = "/account/prevalidate?domainHint=" + WebUtility.UrlEncode(identifier); + using (var requestMessage = new HttpRequestMessage()) + { + requestMessage.Version = new Version(1, 0); + requestMessage.Method = HttpMethod.Get; + requestMessage.RequestUri = new Uri(string.Concat(IdentityBaseUrl, path)); + requestMessage.Headers.Add("Accept", "application/json"); + + HttpResponseMessage response; + try + { + response = await _httpClient.SendAsync(requestMessage); + } + catch (Exception e) + { + throw new ApiException(HandleWebError(e)); + } + if (!response.IsSuccessStatusCode) + { + var error = await HandleErrorAsync(response, false, true); + throw new ApiException(error); + } + var responseJsonString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(responseJsonString); + } + } + + public Task SendAsync(HttpMethod method, string path, bool authed) => + SendAsync(method, path, null, authed, false); + public Task SendAsync(HttpMethod method, string path, TRequest body, bool authed) => + SendAsync(method, path, body, authed, false); + public Task SendAsync(HttpMethod method, string path, bool authed) => + SendAsync(method, path, null, authed, true); + public async Task SendAsync(HttpMethod method, string path, TRequest body, + bool authed, bool hasResponse, Action alterRequest = null, bool logoutOnUnauthorized = true) + { + using (var requestMessage = new HttpRequestMessage()) + { + requestMessage.Version = new Version(1, 0); + requestMessage.Method = method; + + if (!Uri.IsWellFormedUriString(ApiBaseUrl, UriKind.Absolute)) + { + throw new ApiException(new ErrorResponse + { + StatusCode = HttpStatusCode.BadGateway, + //Note: This message is hardcoded until AppResources.resx gets moved into Core.csproj + Message = "One or more URLs saved in the Settings are incorrect. Please revise it and try to log in again." + }); + } + + requestMessage.RequestUri = new Uri(string.Concat(ApiBaseUrl, path)); + + if (body != null) + { + var bodyType = body.GetType(); + if (bodyType == typeof(string)) + { + requestMessage.Content = new StringContent((object)bodyType as string, Encoding.UTF8, + "application/x-www-form-urlencoded; charset=utf-8"); + } + else if (bodyType == typeof(MultipartFormDataContent)) + { + requestMessage.Content = body as MultipartFormDataContent; + } + else + { + requestMessage.Content = new StringContent(JsonConvert.SerializeObject(body, _jsonSettings), + Encoding.UTF8, "application/json"); + } + } + + if (authed) + { + var authHeader = await GetActiveBearerTokenAsync(); + requestMessage.Headers.Add("Authorization", string.Concat("Bearer ", authHeader)); + } + if (hasResponse) + { + requestMessage.Headers.Add("Accept", "application/json"); + } + alterRequest?.Invoke(requestMessage); + + HttpResponseMessage response; + try + { + response = await _httpClient.SendAsync(requestMessage); + } + catch (Exception e) + { + throw new ApiException(HandleWebError(e)); + } + if (hasResponse && response.IsSuccessStatusCode) + { + var responseJsonString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(responseJsonString); + } + else if (!response.IsSuccessStatusCode) + { + var error = await HandleErrorAsync(response, false, authed, logoutOnUnauthorized); + throw new ApiException(error); + } + return (TResponse)(object)null; + } + } + + public async Task DoRefreshTokenAsync() + { + var refreshToken = await _tokenService.GetRefreshTokenAsync(); + if (string.IsNullOrWhiteSpace(refreshToken)) + { + throw new ApiException(); + } + + var decodedToken = _tokenService.DecodeToken(); + var requestMessage = new HttpRequestMessage + { + Version = new Version(1, 0), + RequestUri = new Uri(string.Concat(IdentityBaseUrl, "/connect/token")), + Method = HttpMethod.Post, + Content = new FormUrlEncodedContent(new Dictionary + { + ["grant_type"] = "refresh_token", + ["client_id"] = decodedToken.GetValue("client_id")?.Value(), + ["refresh_token"] = refreshToken + }) + }; + requestMessage.Headers.Add("Accept", "application/json"); + + HttpResponseMessage response; + try + { + response = await _httpClient.SendAsync(requestMessage); + } + catch (Exception e) + { + throw new ApiException(HandleWebError(e)); + } + if (response.IsSuccessStatusCode) + { + var responseJsonString = await response.Content.ReadAsStringAsync(); + var tokenResponse = JsonConvert.DeserializeObject(responseJsonString); + await _tokenService.SetTokensAsync(tokenResponse.AccessToken, tokenResponse.RefreshToken); + return tokenResponse; + } + else + { + var error = await HandleErrorAsync(response, true, true); + throw new ApiException(error); + } + } + + public async Task SendAsync(HttpRequestMessage requestMessage, CancellationToken cancellationToken = default) + { + HttpResponseMessage response; + try + { + response = await _httpClient.SendAsync(requestMessage, cancellationToken); + } + catch (Exception e) + { + throw new ApiException(HandleWebError(e)); + } + if (!response.IsSuccessStatusCode) + { + throw new ApiException(new ErrorResponse + { + StatusCode = response.StatusCode, + Message = $"{requestMessage.RequestUri} error: {(int)response.StatusCode} {response.ReasonPhrase}." + }); + } + return response; + } + + public async Task GetFastmailAccountIdAsync(string apiKey) + { + using (var httpclient = new HttpClient()) + { + HttpResponseMessage response; + try + { + httpclient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); + httpclient.DefaultRequestHeaders.Add("Accept", "application/json"); + response = await httpclient.GetAsync(new Uri("https://api.fastmail.com/jmap/session")); + } + catch (Exception e) + { + throw new ApiException(HandleWebError(e)); + } + if (!response.IsSuccessStatusCode) + { + throw new ApiException(new ErrorResponse + { + StatusCode = response.StatusCode, + Message = $"Fastmail error: {(int)response.StatusCode} {response.ReasonPhrase}." + }); + } + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + return result["primaryAccounts"]?["https://www.fastmail.com/dev/maskedemail"]?.ToString(); + } + } + + private ErrorResponse HandleWebError(Exception e) + { + return new ErrorResponse + { + StatusCode = HttpStatusCode.BadGateway, + Message = "Exception message: " + e.Message + }; + } + + private async Task HandleErrorAsync(HttpResponseMessage response, bool tokenError, + bool authed, bool logoutOnUnauthorized = true) + { + if (authed + && + ( + (logoutOnUnauthorized && response.StatusCode == HttpStatusCode.Unauthorized) + || + response.StatusCode == HttpStatusCode.Forbidden + )) + { + await _logoutCallbackAsync(new Tuple(null, false, true)); + return null; + } + try + { + JObject responseJObject = null; + if (IsJsonResponse(response)) + { + var responseJsonString = await response.Content.ReadAsStringAsync(); + responseJObject = JObject.Parse(responseJsonString); + } + + if (authed && tokenError + && + response.StatusCode == HttpStatusCode.BadRequest + && + responseJObject?["error"]?.ToString() == "invalid_grant") + { + await _logoutCallbackAsync(new Tuple(null, false, true)); + return null; + } + + return new ErrorResponse(responseJObject, response.StatusCode, tokenError); + } + catch + { + return null; + } + } + + private bool IsJsonResponse(HttpResponseMessage response) + { + if (response.Content?.Headers is null) + { + return false; + } + + if (response.Content.Headers.ContentType?.MediaType == "application/json") + { + return true; + } + + return response.Content.Headers.TryGetValues("Content-Type", out var vals) + && + vals?.Any(v => v.Contains("application/json")) is true; + } + + #endregion + } +} diff --git a/src/Maui/Bitwarden/Core/Services/AppIdService.cs b/src/Maui/Bitwarden/Core/Services/AppIdService.cs new file mode 100644 index 000000000..b95fd432f --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/AppIdService.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Abstractions; + +namespace Bit.Core.Services +{ + public class AppIdService : IAppIdService + { + private readonly IStorageService _storageService; + + public AppIdService(IStorageService storageService) + { + _storageService = storageService; + } + + public Task GetAppIdAsync() + { + return MakeAndGetAppIdAsync("appId"); + } + + public Task GetAnonymousAppIdAsync() + { + return MakeAndGetAppIdAsync("anonymousAppId"); + } + + private async Task MakeAndGetAppIdAsync(string key) + { + var existingId = await _storageService.GetAsync(key); + if (existingId != null) + { + return existingId; + } + var guid = Guid.NewGuid().ToString(); + await _storageService.SaveAsync(key, guid); + return guid; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/AuditService.cs b/src/Maui/Bitwarden/Core/Services/AuditService.cs new file mode 100644 index 000000000..d6543e9f3 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/AuditService.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Bit.Core.Models.Response; + +namespace Bit.Core.Services +{ + public class AuditService : IAuditService + { + private const string PwnedPasswordsApi = "https://api.pwnedpasswords.com/range/"; + + private readonly ICryptoFunctionService _cryptoFunctionService; + private readonly IApiService _apiService; + + private HttpClient _httpClient = new HttpClient(); + + public AuditService( + ICryptoFunctionService cryptoFunctionService, + IApiService apiService) + { + _cryptoFunctionService = cryptoFunctionService; + _apiService = apiService; + } + + public async Task PasswordLeakedAsync(string password) + { + var hashBytes = await _cryptoFunctionService.HashAsync(password, Enums.CryptoHashAlgorithm.Sha1); + var hash = BitConverter.ToString(hashBytes).Replace("-", string.Empty).ToUpperInvariant(); + var hashStart = hash.Substring(0, 5); + var hashEnding = hash.Substring(5); + var response = await _httpClient.GetAsync(string.Concat(PwnedPasswordsApi, hashStart)); + var leakedHashes = await response.Content.ReadAsStringAsync(); + var match = leakedHashes.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None) + .FirstOrDefault(v => v.Split(':')[0] == hashEnding); + if (match != null && int.TryParse(match.Split(':')[1], out var matchCount)) + { + return matchCount; + } + return 0; + } + + public async Task> BreachedAccountsAsync(string username) + { + try + { + return await _apiService.GetHibpBreachAsync(username); + } + catch (ApiException e) + { + if (e.Error != null && e.Error.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return new List(); + } + throw; + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/AuthService.cs b/src/Maui/Bitwarden/Core/Services/AuthService.cs new file mode 100644 index 000000000..92cbadd3b --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/AuthService.cs @@ -0,0 +1,642 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; +using Bit.Core.Models.Response; +using Bit.Core.Utilities; + +namespace Bit.Core.Services +{ + public class AuthService : IAuthService + { + private readonly ICryptoService _cryptoService; + private readonly ICryptoFunctionService _cryptoFunctionService; + private readonly IApiService _apiService; + private readonly IStateService _stateService; + private readonly ITokenService _tokenService; + private readonly IAppIdService _appIdService; + private readonly II18nService _i18nService; + private readonly IPlatformUtilsService _platformUtilsService; + private readonly IMessagingService _messagingService; + private readonly IKeyConnectorService _keyConnectorService; + private readonly IPasswordGenerationService _passwordGenerationService; + private readonly IPolicyService _policyService; + private readonly bool _setCryptoKeys; + + private readonly LazyResolve _watchDeviceService = new LazyResolve(); + private SymmetricCryptoKey _key; + + private string _authedUserId; + private MasterPasswordPolicyOptions _masterPasswordPolicy; + private ForcePasswordResetReason? _2faForcePasswordResetReason; + + public AuthService( + ICryptoService cryptoService, + ICryptoFunctionService cryptoFunctionService, + IApiService apiService, + IStateService stateService, + ITokenService tokenService, + IAppIdService appIdService, + II18nService i18nService, + IPlatformUtilsService platformUtilsService, + IMessagingService messagingService, + IVaultTimeoutService vaultTimeoutService, + IKeyConnectorService keyConnectorService, + IPasswordGenerationService passwordGenerationService, + IPolicyService policyService, + bool setCryptoKeys = true) + { + _cryptoService = cryptoService; + _cryptoFunctionService = cryptoFunctionService; + _apiService = apiService; + _stateService = stateService; + _tokenService = tokenService; + _appIdService = appIdService; + _i18nService = i18nService; + _platformUtilsService = platformUtilsService; + _messagingService = messagingService; + _keyConnectorService = keyConnectorService; + _passwordGenerationService = passwordGenerationService; + _policyService = policyService; + _setCryptoKeys = setCryptoKeys; + + TwoFactorProviders = new Dictionary(); + TwoFactorProviders.Add(TwoFactorProviderType.Authenticator, new TwoFactorProvider + { + Type = TwoFactorProviderType.Authenticator, + Priority = 1, + Sort = 1 + }); + TwoFactorProviders.Add(TwoFactorProviderType.YubiKey, new TwoFactorProvider + { + Type = TwoFactorProviderType.YubiKey, + Priority = 3, + Sort = 2, + Premium = true + }); + TwoFactorProviders.Add(TwoFactorProviderType.Duo, new TwoFactorProvider + { + Type = TwoFactorProviderType.Duo, + Name = "Duo", + Priority = 2, + Sort = 3, + Premium = true + }); + TwoFactorProviders.Add(TwoFactorProviderType.OrganizationDuo, new TwoFactorProvider + { + Type = TwoFactorProviderType.OrganizationDuo, + Name = "Duo (Organization)", + Priority = 10, + Sort = 4 + }); + TwoFactorProviders.Add(TwoFactorProviderType.Fido2WebAuthn, new TwoFactorProvider + { + Type = TwoFactorProviderType.Fido2WebAuthn, + Priority = 4, + Sort = 5, + Premium = true + }); + TwoFactorProviders.Add(TwoFactorProviderType.Email, new TwoFactorProvider + { + Type = TwoFactorProviderType.Email, + Priority = 0, + Sort = 6, + }); + } + + public string Email { get; set; } + public string CaptchaToken { get; set; } + public string MasterPasswordHash { get; set; } + public string LocalMasterPasswordHash { get; set; } + public string AuthRequestId { get; set; } + public string Code { get; set; } + public string CodeVerifier { get; set; } + public string SsoRedirectUrl { get; set; } + public Dictionary TwoFactorProviders { get; set; } + public Dictionary> TwoFactorProvidersData { get; set; } + public TwoFactorProviderType? SelectedTwoFactorProviderType { get; set; } + + public void Init() + { + TwoFactorProviders[TwoFactorProviderType.Email].Name = _i18nService.T("Email"); + TwoFactorProviders[TwoFactorProviderType.Email].Description = _i18nService.T("EmailDesc"); + TwoFactorProviders[TwoFactorProviderType.Authenticator].Name = _i18nService.T("AuthenticatorAppTitle"); + TwoFactorProviders[TwoFactorProviderType.Authenticator].Description = + _i18nService.T("AuthenticatorAppDesc"); + TwoFactorProviders[TwoFactorProviderType.Duo].Description = _i18nService.T("DuoDesc"); + TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].Name = + string.Format("Duo ({0})", _i18nService.T("Organization")); + TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].Description = + _i18nService.T("DuoOrganizationDesc"); + TwoFactorProviders[TwoFactorProviderType.Fido2WebAuthn].Name = _i18nService.T("Fido2Title"); + TwoFactorProviders[TwoFactorProviderType.Fido2WebAuthn].Description = _i18nService.T("Fido2Desc"); + TwoFactorProviders[TwoFactorProviderType.YubiKey].Name = _i18nService.T("YubiKeyTitle"); + TwoFactorProviders[TwoFactorProviderType.YubiKey].Description = _i18nService.T("YubiKeyDesc"); + } + + public async Task LogInAsync(string email, string masterPassword, string captchaToken) + { + SelectedTwoFactorProviderType = null; + _2faForcePasswordResetReason = null; + var key = await MakePreloginKeyAsync(masterPassword, email); + var hashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key); + var localHashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key, HashPurpose.LocalAuthorization); + var result = await LogInHelperAsync(email, hashedPassword, localHashedPassword, null, null, null, key, null, null, null, captchaToken); + + if (await RequirePasswordChangeAsync(email, masterPassword)) + { + if (!string.IsNullOrEmpty(_authedUserId)) + { + // Authentication was successful, save the WeakMasterPasswordOnLogin flag for the user + result.ForcePasswordReset = true; + await _stateService.SetForcePasswordResetReasonAsync( + ForcePasswordResetReason.WeakMasterPasswordOnLogin, _authedUserId); + } + else + { + // Authentication not fully successful (likely 2FA), store flag for LogInTwoFactorAsync() + _2faForcePasswordResetReason = ForcePasswordResetReason.WeakMasterPasswordOnLogin; + } + } + + return result; + } + + /// + /// Evaluates the supplied master password against the master password policy provided by the Identity response. + /// + /// + /// + /// True if the master password does NOT meet any policy requirements, false otherwise (or if no policy present) + private async Task RequirePasswordChangeAsync(string email, string masterPassword) + { + // No policy with EnforceOnLogin enabled, we're done. + if (!(_masterPasswordPolicy is { EnforceOnLogin: true })) + { + return false; + } + + var strength = _passwordGenerationService.PasswordStrength( + masterPassword, + _passwordGenerationService.GetPasswordStrengthUserInput(email) + )?.Score; + + if (!strength.HasValue) + { + return false; + } + + return !await _policyService.EvaluateMasterPassword(strength.Value, masterPassword, _masterPasswordPolicy); + } + + public async Task LogInPasswordlessAsync(string email, string accessCode, string authRequestId, byte[] decryptionKey, string userKeyCiphered, string localHashedPasswordCiphered) + { + var decKey = await _cryptoService.RsaDecryptAsync(userKeyCiphered, decryptionKey); + var decPasswordHash = await _cryptoService.RsaDecryptAsync(localHashedPasswordCiphered, decryptionKey); + return await LogInHelperAsync(email, accessCode, Encoding.UTF8.GetString(decPasswordHash), null, null, null, new SymmetricCryptoKey(decKey), null, null, + null, null, authRequestId: authRequestId); + } + + public async Task LogInSsoAsync(string code, string codeVerifier, string redirectUrl, string orgId) + { + SelectedTwoFactorProviderType = null; + return await LogInHelperAsync(null, null, null, code, codeVerifier, redirectUrl, null, orgId: orgId); + } + + public async Task LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, + string captchaToken, bool? remember = null) + { + if (captchaToken != null) + { + CaptchaToken = captchaToken; + } + var result = await LogInHelperAsync(Email, MasterPasswordHash, LocalMasterPasswordHash, Code, CodeVerifier, SsoRedirectUrl, _key, + twoFactorProvider, twoFactorToken, remember, CaptchaToken, authRequestId: AuthRequestId); + + // If we successfully authenticated and we have a saved _2faForcePasswordResetReason reason from LogInAsync() + if (!string.IsNullOrEmpty(_authedUserId) && _2faForcePasswordResetReason.HasValue) + { + // Save the forcePasswordReset reason with the state service to force a password reset for the user + result.ForcePasswordReset = true; + await _stateService.SetForcePasswordResetReasonAsync( + _2faForcePasswordResetReason, _authedUserId); + } + + return result; + } + + public async Task LogInCompleteAsync(string email, string masterPassword, + TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null) + { + SelectedTwoFactorProviderType = null; + var key = await MakePreloginKeyAsync(masterPassword, email); + var hashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key); + var localHashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key, HashPurpose.LocalAuthorization); + return await LogInHelperAsync(email, hashedPassword, localHashedPassword, null, null, null, key, twoFactorProvider, + twoFactorToken, remember); + } + + public async Task LogInSsoCompleteAsync(string code, string codeVerifier, string redirectUrl, + TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null) + { + SelectedTwoFactorProviderType = null; + return await LogInHelperAsync(null, null, null, code, codeVerifier, redirectUrl, null, twoFactorProvider, + twoFactorToken, remember); + } + + public void LogOut(Action callback) + { + callback.Invoke(); + _messagingService.Send(AccountsManagerMessageCommands.LOGGED_OUT); + _watchDeviceService.Value.SyncDataToWatchAsync().FireAndForget(); + } + + public List GetSupportedTwoFactorProviders() + { + var providers = new List(); + if (TwoFactorProvidersData == null) + { + return providers; + } + if (TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.OrganizationDuo) && + _platformUtilsService.SupportsDuo()) + { + providers.Add(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]); + } + if (TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.Authenticator)) + { + providers.Add(TwoFactorProviders[TwoFactorProviderType.Authenticator]); + } + if (TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.YubiKey)) + { + providers.Add(TwoFactorProviders[TwoFactorProviderType.YubiKey]); + } + if (TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.Duo) && _platformUtilsService.SupportsDuo()) + { + providers.Add(TwoFactorProviders[TwoFactorProviderType.Duo]); + } + if (TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.Fido2WebAuthn) && + _platformUtilsService.SupportsFido2()) + { + providers.Add(TwoFactorProviders[TwoFactorProviderType.Fido2WebAuthn]); + } + if (TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.Email)) + { + providers.Add(TwoFactorProviders[TwoFactorProviderType.Email]); + } + return providers; + } + + public TwoFactorProviderType? GetDefaultTwoFactorProvider(bool fido2Supported) + { + if (TwoFactorProvidersData == null) + { + return null; + } + if (SelectedTwoFactorProviderType != null && + TwoFactorProvidersData.ContainsKey(SelectedTwoFactorProviderType.Value)) + { + return SelectedTwoFactorProviderType.Value; + } + TwoFactorProviderType? providerType = null; + var providerPriority = -1; + foreach (var providerKvp in TwoFactorProvidersData) + { + if (TwoFactorProviders.ContainsKey(providerKvp.Key)) + { + var provider = TwoFactorProviders[providerKvp.Key]; + if (provider.Priority > providerPriority) + { + if (providerKvp.Key == TwoFactorProviderType.Fido2WebAuthn && !fido2Supported) + { + continue; + } + providerType = providerKvp.Key; + providerPriority = provider.Priority; + } + } + } + return providerType; + } + + public bool AuthingWithSso() + { + return Code != null && CodeVerifier != null && SsoRedirectUrl != null; + } + + public bool AuthingWithPassword() + { + return Email != null && MasterPasswordHash != null; + } + + // Helpers + + private async Task MakePreloginKeyAsync(string masterPassword, string email) + { + email = email.Trim().ToLower(); + KdfConfig kdfConfig = KdfConfig.Default; + try + { + var preloginResponse = await _apiService.PostPreloginAsync(new PreloginRequest { Email = email }); + if (preloginResponse != null) + { + kdfConfig = preloginResponse.KdfConfig; + } + } + catch (ApiException e) + { + if (e.Error == null || e.Error.StatusCode != System.Net.HttpStatusCode.NotFound) + { + throw; + } + } + return await _cryptoService.MakeKeyAsync(masterPassword, email, kdfConfig); + } + + private async Task LogInHelperAsync(string email, string hashedPassword, string localHashedPassword, + string code, string codeVerifier, string redirectUrl, SymmetricCryptoKey key, + TwoFactorProviderType? twoFactorProvider = null, string twoFactorToken = null, bool? remember = null, + string captchaToken = null, string orgId = null, string authRequestId = null) + { + var storedTwoFactorToken = await _tokenService.GetTwoFactorTokenAsync(email); + var appId = await _appIdService.GetAppIdAsync(); + var deviceRequest = new DeviceRequest(appId, _platformUtilsService); + + string[] emailPassword; + string[] codeCodeVerifier; + if (email != null && hashedPassword != null) + { + emailPassword = new[] { email, hashedPassword }; + } + else + { + emailPassword = null; + } + if (code != null && codeVerifier != null && redirectUrl != null) + { + codeCodeVerifier = new[] { code, codeVerifier, redirectUrl }; + } + else + { + codeCodeVerifier = null; + } + + TokenRequest request; + if (twoFactorToken != null && twoFactorProvider != null) + { + request = new TokenRequest(emailPassword, codeCodeVerifier, twoFactorProvider, twoFactorToken, remember, + captchaToken, deviceRequest, authRequestId); + } + else if (storedTwoFactorToken != null) + { + request = new TokenRequest(emailPassword, codeCodeVerifier, TwoFactorProviderType.Remember, + storedTwoFactorToken, false, captchaToken, deviceRequest, authRequestId); + } + else if (authRequestId != null) + { + request = new TokenRequest(emailPassword, null, null, null, false, null, deviceRequest, authRequestId); + } + else + { + request = new TokenRequest(emailPassword, codeCodeVerifier, null, null, false, captchaToken, deviceRequest); + } + + var response = await _apiService.PostIdentityTokenAsync(request); + ClearState(); + var result = new AuthResult { TwoFactor = response.TwoFactorNeeded, CaptchaSiteKey = response.CaptchaResponse?.SiteKey }; + + if (result.CaptchaNeeded) + { + return result; + } + + if (result.TwoFactor) + { + // Two factor required. + Email = email; + MasterPasswordHash = hashedPassword; + LocalMasterPasswordHash = localHashedPassword; + AuthRequestId = authRequestId; + Code = code; + CodeVerifier = codeVerifier; + SsoRedirectUrl = redirectUrl; + _key = _setCryptoKeys ? key : null; + TwoFactorProvidersData = response.TwoFactorResponse.TwoFactorProviders2; + result.TwoFactorProviders = response.TwoFactorResponse.TwoFactorProviders2; + CaptchaToken = response.TwoFactorResponse.CaptchaToken; + _masterPasswordPolicy = response.TwoFactorResponse.MasterPasswordPolicy; + await _tokenService.ClearTwoFactorTokenAsync(email); + return result; + } + + var tokenResponse = response.TokenResponse; + result.ResetMasterPassword = tokenResponse.ResetMasterPassword; + result.ForcePasswordReset = tokenResponse.ForcePasswordReset; + _masterPasswordPolicy = tokenResponse.MasterPasswordPolicy; + if (tokenResponse.TwoFactorToken != null) + { + await _tokenService.SetTwoFactorTokenAsync(tokenResponse.TwoFactorToken, email); + } + await _tokenService.SetAccessTokenAsync(tokenResponse.AccessToken, true); + await _stateService.AddAccountAsync( + new Account( + new Account.AccountProfile() + { + UserId = _tokenService.GetUserId(), + Email = _tokenService.GetEmail(), + Name = _tokenService.GetName(), + KdfType = tokenResponse.Kdf, + KdfIterations = tokenResponse.KdfIterations, + KdfMemory = tokenResponse.KdfMemory, + KdfParallelism = tokenResponse.KdfParallelism, + HasPremiumPersonally = _tokenService.GetPremium(), + ForcePasswordResetReason = result.ForcePasswordReset + ? ForcePasswordResetReason.AdminForcePasswordReset + : (ForcePasswordResetReason?)null, + }, + new Account.AccountTokens() + { + AccessToken = tokenResponse.AccessToken, + RefreshToken = tokenResponse.RefreshToken, + } + ) + ); + _messagingService.Send("accountAdded"); + if (_setCryptoKeys) + { + if (key != null) + { + await _cryptoService.SetKeyAsync(key); + } + + if (localHashedPassword != null) + { + await _cryptoService.SetKeyHashAsync(localHashedPassword); + } + + if (code == null || tokenResponse.Key != null) + { + if (tokenResponse.KeyConnectorUrl != null) + { + await _keyConnectorService.GetAndSetKey(tokenResponse.KeyConnectorUrl); + } + + await _cryptoService.SetEncKeyAsync(tokenResponse.Key); + + // User doesn't have a key pair yet (old account), let's generate one for them. + if (tokenResponse.PrivateKey == null) + { + try + { + var keyPair = await _cryptoService.MakeKeyPairAsync(); + await _apiService.PostAccountKeysAsync(new KeysRequest + { + PublicKey = keyPair.Item1, + EncryptedPrivateKey = keyPair.Item2.EncryptedString + }); + tokenResponse.PrivateKey = keyPair.Item2.EncryptedString; + } + catch { } + } + + await _cryptoService.SetEncPrivateKeyAsync(tokenResponse.PrivateKey); + } + else if (tokenResponse.KeyConnectorUrl != null) + { + // SSO Key Connector Onboarding + var password = await _cryptoFunctionService.RandomBytesAsync(64); + var k = await _cryptoService.MakeKeyAsync(Convert.ToBase64String(password), _tokenService.GetEmail(), tokenResponse.KdfConfig); + var keyConnectorRequest = new KeyConnectorUserKeyRequest(k.EncKeyB64); + await _cryptoService.SetKeyAsync(k); + + var encKey = await _cryptoService.MakeEncKeyAsync(k); + await _cryptoService.SetEncKeyAsync(encKey.Item2.EncryptedString); + var keyPair = await _cryptoService.MakeKeyPairAsync(); + + try + { + await _apiService.PostUserKeyToKeyConnector(tokenResponse.KeyConnectorUrl, keyConnectorRequest); + } + catch (Exception e) + { + throw new Exception("Unable to reach Key Connector", e); + } + + var keys = new KeysRequest + { + PublicKey = keyPair.Item1, + EncryptedPrivateKey = keyPair.Item2.EncryptedString + }; + var setPasswordRequest = new SetKeyConnectorKeyRequest( + encKey.Item2.EncryptedString, keys, tokenResponse.KdfConfig, orgId + ); + await _apiService.PostSetKeyConnectorKey(setPasswordRequest); + } + + } + + _authedUserId = _tokenService.GetUserId(); + await _stateService.SetBiometricLockedAsync(false); + _messagingService.Send("loggedIn"); + return result; + } + + private void ClearState() + { + _key = null; + Email = null; + CaptchaToken = null; + MasterPasswordHash = null; + AuthRequestId = null; + Code = null; + CodeVerifier = null; + SsoRedirectUrl = null; + TwoFactorProvidersData = null; + SelectedTwoFactorProviderType = null; + _masterPasswordPolicy = null; + _authedUserId = null; + } + + public async Task> GetPasswordlessLoginRequestsAsync() + { + var response = await _apiService.GetAuthRequestAsync(); + return await PopulateFingerprintPhrasesAsync(response); + } + + public async Task> GetActivePasswordlessLoginRequestsAsync() + { + var requests = await GetPasswordlessLoginRequestsAsync(); + var activeRequests = requests.Where(r => !r.IsAnswered && !r.IsExpired).OrderByDescending(r => r.CreationDate).ToList(); + return await PopulateFingerprintPhrasesAsync(activeRequests); + } + + public async Task GetPasswordlessLoginRequestByIdAsync(string id) + { + var response = await _apiService.GetAuthRequestAsync(id); + return await PopulateFingerprintPhraseAsync(response, await _stateService.GetEmailAsync()); + } + + public async Task GetPasswordlessLoginResponseAsync(string id, string accessCode) + { + return await _apiService.GetAuthResponseAsync(id, accessCode); + } + + public async Task PasswordlessLoginAsync(string id, string pubKey, bool requestApproved) + { + var publicKey = CoreHelpers.Base64UrlDecode(pubKey); + var masterKey = await _cryptoService.GetKeyAsync(); + var encryptedKey = await _cryptoService.RsaEncryptAsync(masterKey.EncKey, publicKey); + var encryptedMasterPassword = await _cryptoService.RsaEncryptAsync(Encoding.UTF8.GetBytes(await _stateService.GetKeyHashAsync()), publicKey); + var deviceId = await _appIdService.GetAppIdAsync(); + var response = await _apiService.PutAuthRequestAsync(id, encryptedKey.EncryptedString, encryptedMasterPassword.EncryptedString, deviceId, requestApproved); + return await PopulateFingerprintPhraseAsync(response, await _stateService.GetEmailAsync()); + } + + public async Task PasswordlessCreateLoginRequestAsync(string email) + { + var deviceId = await _appIdService.GetAppIdAsync(); + var keyPair = await _cryptoFunctionService.RsaGenerateKeyPairAsync(2048); + var generatedFingerprintPhrase = await _cryptoService.GetFingerprintAsync(email, keyPair.Item1); + var fingerprintPhrase = string.Join("-", generatedFingerprintPhrase); + var publicB64 = Convert.ToBase64String(keyPair.Item1); + var accessCode = await _passwordGenerationService.GeneratePasswordAsync(PasswordGenerationOptions.CreateDefault.WithLength(25)); + var passwordlessCreateLoginRequest = new PasswordlessCreateLoginRequest(email, publicB64, deviceId, accessCode, AuthRequestType.AuthenticateAndUnlock, fingerprintPhrase); + var response = await _apiService.PostCreateRequestAsync(passwordlessCreateLoginRequest); + + if (response != null) + { + response.RequestKeyPair = keyPair; + response.RequestAccessCode = accessCode; + response.FingerprintPhrase = fingerprintPhrase; + } + + return response; + } + + private async Task> PopulateFingerprintPhrasesAsync(List passwordlessLoginList) + { + if (passwordlessLoginList == null) + { + return null; + } + var userEmail = await _stateService.GetEmailAsync(); + foreach (var passwordlessLogin in passwordlessLoginList) + { + await PopulateFingerprintPhraseAsync(passwordlessLogin, userEmail); + } + return passwordlessLoginList; + } + + private async Task PopulateFingerprintPhraseAsync(PasswordlessLoginResponse passwordlessLogin, string userEmail) + { + passwordlessLogin.FingerprintPhrase = string.Join("-", await _cryptoService.GetFingerprintAsync(userEmail, CoreHelpers.Base64UrlDecode(passwordlessLogin.PublicKey))); + return passwordlessLogin; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/AzureFileUploadService.cs b/src/Maui/Bitwarden/Core/Services/AzureFileUploadService.cs new file mode 100644 index 000000000..4d18c1246 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/AzureFileUploadService.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using Bit.Core.Abstractions; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; + +namespace Bit.Core.Services +{ + public class AzureFileUploadService : IAzureFileUploadService + { + private const long MAX_SINGLE_BLOB_UPLOAD_SIZE = 256 * 1024 * 1024; // 256 MiB + private const int MAX_BLOCKS_PER_BLOB = 50000; + private const decimal MAX_MOBILE_BLOCK_SIZE = 5 * 1024 * 1024; // 5 MB + + private readonly HttpClient _httpClient = new HttpClient(); + + public AzureFileUploadService() + { + _httpClient.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue() + { + NoCache = true, + }; + } + + public async Task Upload(string uri, EncByteArray data, Func> renewalCallback) + { + if (data?.Buffer?.Length <= MAX_SINGLE_BLOB_UPLOAD_SIZE) + { + await AzureUploadBlob(uri, data); + } + else + { + await AzureUploadBlocks(uri, data, renewalCallback); + } + } + + private async Task AzureUploadBlob(string uri, EncByteArray data) + { + using (var requestMessage = new HttpRequestMessage()) + { + var uriBuilder = new UriBuilder(uri); + var paramValues = HttpUtility.ParseQueryString(uriBuilder.Query); + + requestMessage.Headers.Add("x-ms-date", DateTime.UtcNow.ToString("R")); + requestMessage.Headers.Add("x-ms-version", paramValues["sv"]); + requestMessage.Headers.Add("x-ms-blob-type", "BlockBlob"); + + requestMessage.Content = new ByteArrayContent(data.Buffer); + requestMessage.Version = new Version(1, 0); + requestMessage.Method = HttpMethod.Put; + requestMessage.RequestUri = uriBuilder.Uri; + + var blobResponse = await _httpClient.SendAsync(requestMessage); + + if (blobResponse.StatusCode != HttpStatusCode.Created) + { + throw new Exception("Failed to create Azure blob"); + } + } + } + + private async Task AzureUploadBlocks(string uri, EncByteArray data, Func> renewalFunc) + { + _httpClient.Timeout = TimeSpan.FromHours(3); + var baseParams = HttpUtility.ParseQueryString(CoreHelpers.GetUri(uri).Query); + var blockSize = MaxBlockSize(baseParams["sv"]); + var blockIndex = 0; + var numBlocks = Math.Ceiling((decimal)data.Buffer.Length / blockSize); + var blocksStaged = new List(); + + if (numBlocks > MAX_BLOCKS_PER_BLOB) + { + throw new Exception($"Cannot upload file, exceeds maximum size of {blockSize * MAX_BLOCKS_PER_BLOB}"); + } + + while (blockIndex < numBlocks) + { + uri = await RenewUriIfNecessary(uri, renewalFunc); + var blockUriBuilder = new UriBuilder(uri); + var blockId = EncodeBlockId(blockIndex); + var blockParams = HttpUtility.ParseQueryString(blockUriBuilder.Query); + blockParams.Add("comp", "block"); + blockParams.Add("blockid", blockId); + blockUriBuilder.Query = blockParams.ToString(); + + using (var requestMessage = new HttpRequestMessage()) + { + requestMessage.Headers.Add("x-ms-date", DateTime.UtcNow.ToString("R")); + requestMessage.Headers.Add("x-ms-version", baseParams["sv"]); + requestMessage.Headers.Add("x-ms-blob-type", "BlockBlob"); + + requestMessage.Content = new ByteArrayContent(data.Buffer.Skip(blockIndex * blockSize).Take(blockSize).ToArray()); + requestMessage.Version = new Version(1, 0); + requestMessage.Method = HttpMethod.Put; + requestMessage.RequestUri = blockUriBuilder.Uri; + + var blockResponse = await _httpClient.SendAsync(requestMessage); + + if (blockResponse.StatusCode != HttpStatusCode.Created) + { + throw new Exception("Failed to create Azure block"); + } + } + + blocksStaged.Add(blockId); + blockIndex++; + } + + using (var requestMessage = new HttpRequestMessage()) + { + uri = await RenewUriIfNecessary(uri, renewalFunc); + var blockListXml = GenerateBlockListXml(blocksStaged); + var blockListUriBuilder = new UriBuilder(uri); + var blockListParams = HttpUtility.ParseQueryString(blockListUriBuilder.Query); + blockListParams.Add("comp", "blocklist"); + blockListUriBuilder.Query = blockListParams.ToString(); + + requestMessage.Headers.Add("x-ms-date", DateTime.UtcNow.ToString("R")); + requestMessage.Headers.Add("x-ms-version", baseParams["sv"]); + + requestMessage.Content = new StringContent(blockListXml); + requestMessage.Version = new Version(1, 0); + requestMessage.Method = HttpMethod.Put; + requestMessage.RequestUri = blockListUriBuilder.Uri; + + var blockListResponse = await _httpClient.SendAsync(requestMessage); + + if (blockListResponse.StatusCode != HttpStatusCode.Created) + { + throw new Exception("Failed to PUT Azure block list"); + } + } + } + + private async Task RenewUriIfNecessary(string uri, Func> renewalFunc) + { + var uriParams = HttpUtility.ParseQueryString(CoreHelpers.GetUri(uri).Query); + + if (DateTime.TryParse(uriParams.Get("se") ?? "", out DateTime expiry) && expiry < DateTime.UtcNow.AddSeconds(1)) + { + return await renewalFunc(); + } + return uri; + } + + private string GenerateBlockListXml(List blocksStaged) + { + var xml = new StringBuilder(""); + foreach (var blockId in blocksStaged) + { + xml.Append($"{blockId}"); + } + xml.Append(""); + return xml.ToString(); + } + + private string EncodeBlockId(int index) + { + // Encoded blockId max size is 64, so pre-encoding max size is 48 + var paddedString = index.ToString("D48"); + return Convert.ToBase64String(Encoding.UTF8.GetBytes(paddedString)); + } + + private int MaxBlockSize(string version) + { + long maxSize = 4194304L; // 4 MiB + if (CompareAzureVersions(version, "2019-12-12") >= 0) + { + maxSize = 4194304000L; // 4000 MiB + } + else if (CompareAzureVersions(version, "2016-05-31") >= 0) + { + maxSize = 104857600L; // 100 MiB + } + + return maxSize > MAX_MOBILE_BLOCK_SIZE ? (int)MAX_MOBILE_BLOCK_SIZE : (int)maxSize; + } + + private int CompareAzureVersions(string a, string b) + { + var v1Parts = a.Split('-').Select(p => int.Parse(p)); + var v2Parts = b.Split('-').Select(p => int.Parse(p)); + + return a[0] != b[0] ? a[0] - b[0] : + a[1] != b[1] ? a[1] - b[1] : + a[2] != b[2] ? a[2] - b[2] : + 0; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/BitwardenFileUploadService.cs b/src/Maui/Bitwarden/Core/Services/BitwardenFileUploadService.cs new file mode 100644 index 000000000..bb0c29073 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/BitwardenFileUploadService.cs @@ -0,0 +1,29 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Threading.Tasks; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Services +{ + public class BitwardenFileUploadService + { + public BitwardenFileUploadService(ApiService apiService) + { + _apiService = apiService; + } + + private readonly ApiService _apiService; + + public async Task Upload(string encryptedFileName, EncByteArray encryptedFileData, Func apiCall) + { + var fd = new MultipartFormDataContent($"--BWMobileFormBoundary{DateTime.UtcNow.Ticks}") + { + { new ByteArrayContent(encryptedFileData.Buffer) { Headers = { ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet) } }, "data", encryptedFileName } + }; + + await apiCall(fd); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/BroadcasterService.cs b/src/Maui/Bitwarden/Core/Services/BroadcasterService.cs new file mode 100644 index 000000000..782529335 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/BroadcasterService.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Models.Domain; + +namespace Bit.App.Services +{ + public class BroadcasterService : IBroadcasterService + { + private readonly ILogger _logger; + private readonly Dictionary> _subscribers = new Dictionary>(); + private object _myLock = new object(); + + public BroadcasterService(ILogger logger) + { + _logger = logger; + } + + public void Send(Message message) + { + lock (_myLock) + { + foreach (var sub in _subscribers) + { + Task.Run(() => + { + try + { + sub.Value(message); + } + catch (Exception ex) + { + _logger.Exception(ex); + } + }); + } + } + } + + public void Send(Message message, string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + return; + } + + lock (_myLock) + { + if (_subscribers.TryGetValue(id, out var action)) + { + Task.Run(() => + { + try + { + action(message); + } + catch (Exception ex) + { + _logger.Exception(ex); + } + }); + } + } + } + + public void Subscribe(string id, Action messageCallback) + { + lock (_myLock) + { + _subscribers[id] = messageCallback; + } + } + + public void Unsubscribe(string id) + { + lock (_myLock) + { + if (_subscribers.ContainsKey(id)) + { + _subscribers.Remove(id); + } + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/CipherService.cs b/src/Maui/Bitwarden/Core/Services/CipherService.cs new file mode 100644 index 000000000..b56c7f0cf --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/CipherService.cs @@ -0,0 +1,1345 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; +using Bit.Core.Models.Response; +using Bit.Core.Models.View; +using Bit.Core.Utilities; + +namespace Bit.Core.Services +{ + public class CipherService : ICipherService + { + private readonly string[] _ignoredSearchTerms = new string[] { "com", "net", "org", "android", + "io", "co", "uk", "au", "nz", "fr", "de", "tv", "info", "app", "apps", "eu", "me", "dev", "jp", "mobile" }; + private List _decryptedCipherCache; + private readonly ICryptoService _cryptoService; + private readonly IStateService _stateService; + private readonly ISettingsService _settingsService; + private readonly IApiService _apiService; + private readonly IFileUploadService _fileUploadService; + private readonly IStorageService _storageService; + private readonly II18nService _i18nService; + private readonly Func _searchService; + private readonly string _clearCipherCacheKey; + private readonly string[] _allClearCipherCacheKeys; + private Dictionary> _domainMatchBlacklist = new Dictionary> + { + ["google.com"] = new HashSet { "script.google.com" } + }; + private readonly HttpClient _httpClient = new HttpClient(); + private Task> _getAllDecryptedTask; + + public CipherService( + ICryptoService cryptoService, + IStateService stateService, + ISettingsService settingsService, + IApiService apiService, + IFileUploadService fileUploadService, + IStorageService storageService, + II18nService i18nService, + Func searchService, + string clearCipherCacheKey, + string[] allClearCipherCacheKeys) + { + _cryptoService = cryptoService; + _stateService = stateService; + _settingsService = settingsService; + _apiService = apiService; + _fileUploadService = fileUploadService; + _storageService = storageService; + _i18nService = i18nService; + _searchService = searchService; + _clearCipherCacheKey = clearCipherCacheKey; + _allClearCipherCacheKeys = allClearCipherCacheKeys; + } + + private List DecryptedCipherCache + { + get => _decryptedCipherCache; + set + { + if (value == null) + { + _decryptedCipherCache?.Clear(); + } + _decryptedCipherCache = value; + if (_searchService != null) + { + if (value == null) + { + _searchService().ClearIndex(); + } + else + { + _searchService().IndexCiphersAsync(); + } + } + } + } + + public async Task ClearCacheAsync() + { + DecryptedCipherCache = null; + if (_allClearCipherCacheKeys != null && _allClearCipherCacheKeys.Length > 0) + { + foreach (var key in _allClearCipherCacheKeys) + { + await _storageService.SaveAsync(key, true); + } + } + } + + public async Task EncryptAsync(CipherView model, SymmetricCryptoKey key = null, + Cipher originalCipher = null) + { + // Adjust password history + if (model.Id != null) + { + if (originalCipher == null) + { + originalCipher = await GetAsync(model.Id); + } + if (originalCipher != null) + { + var existingCipher = await originalCipher.DecryptAsync(); + if (model.PasswordHistory == null) + { + model.PasswordHistory = new List(); + } + if (model.Type == CipherType.Login && existingCipher.Type == CipherType.Login) + { + if (!string.IsNullOrWhiteSpace(existingCipher.Login.Password) && + existingCipher.Login.Password != model.Login.Password) + { + var now = DateTime.UtcNow; + var ph = new PasswordHistoryView + { + Password = existingCipher.Login.Password, + LastUsedDate = now + }; + model.Login.PasswordRevisionDate = now; + model.PasswordHistory.Insert(0, ph); + } + else + { + model.Login.PasswordRevisionDate = existingCipher.Login.PasswordRevisionDate; + } + } + if (existingCipher.HasFields) + { + var existingHiddenFields = existingCipher.Fields.Where(f => + f.Type == FieldType.Hidden && !string.IsNullOrWhiteSpace(f.Name) && + !string.IsNullOrWhiteSpace(f.Value)); + var hiddenFields = model.Fields?.Where(f => + f.Type == FieldType.Hidden && !string.IsNullOrWhiteSpace(f.Name)) ?? + new List(); + foreach (var ef in existingHiddenFields) + { + var matchedField = hiddenFields.FirstOrDefault(f => f.Name == ef.Name); + if (matchedField == null || matchedField.Value != ef.Value) + { + var ph = new PasswordHistoryView + { + Password = string.Format("{0}: {1}", ef.Name, ef.Value), + LastUsedDate = DateTime.UtcNow + }; + model.PasswordHistory.Insert(0, ph); + } + } + } + } + if (!model.PasswordHistory?.Any() ?? false) + { + model.PasswordHistory = null; + } + else if (model.PasswordHistory != null && model.PasswordHistory.Count > 5) + { + model.PasswordHistory = model.PasswordHistory.Take(5).ToList(); + } + } + + var cipher = new Cipher + { + Id = model.Id, + FolderId = model.FolderId, + Favorite = model.Favorite, + OrganizationId = model.OrganizationId, + Type = model.Type, + CollectionIds = model.CollectionIds, + CreationDate = model.CreationDate, + RevisionDate = model.RevisionDate, + Reprompt = model.Reprompt + }; + + if (key == null && cipher.OrganizationId != null) + { + key = await _cryptoService.GetOrgKeyAsync(cipher.OrganizationId); + if (key == null) + { + throw new Exception("Cannot encrypt cipher for organization. No key."); + } + } + + var tasks = new List + { + EncryptObjPropertyAsync(model, cipher, new HashSet + { + "Name", + "Notes" + }, key), + EncryptCipherDataAsync(cipher, model, key), + EncryptFieldsAsync(model.Fields, key, cipher), + EncryptPasswordHistoriesAsync(model.PasswordHistory, key, cipher), + EncryptAttachmentsAsync(model.Attachments, key, cipher) + }; + await Task.WhenAll(tasks); + return cipher; + } + + public async Task GetAsync(string id) + { + var localData = await _stateService.GetLocalDataAsync(); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); + if (!ciphers?.ContainsKey(id) ?? true) + { + return null; + } + return new Cipher(ciphers[id], false, + localData?.ContainsKey(id) ?? false ? localData[id] : null); + } + + public async Task> GetAllAsync() + { + var localData = await _stateService.GetLocalDataAsync(); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); + var response = ciphers?.Select(c => new Cipher(c.Value, false, + localData?.ContainsKey(c.Key) ?? false ? localData[c.Key] : null)); + return response?.ToList() ?? new List(); + } + + public async Task> GetAllDecryptedAsync(Func filter = null) + { + if (_clearCipherCacheKey != null) + { + var clearCache = await _storageService.GetAsync(_clearCipherCacheKey); + if (clearCache.GetValueOrDefault()) + { + DecryptedCipherCache = null; + await _storageService.RemoveAsync(_clearCipherCacheKey); + } + } + if (DecryptedCipherCache != null && filter is null) + { + return DecryptedCipherCache; + } + if (_getAllDecryptedTask != null && !_getAllDecryptedTask.IsCompleted && !_getAllDecryptedTask.IsFaulted) + { + return await _getAllDecryptedTask; + } + async Task> doTask() + { + try + { + var hashKey = await _cryptoService.HasKeyAsync(); + if (!hashKey) + { + throw new Exception("No key."); + } + var decCiphers = new List(); + async Task decryptAndAddCipherAsync(Cipher cipher) + { + var c = await cipher.DecryptAsync(); + decCiphers.Add(c); + } + var tasks = new List(); + IEnumerable ciphers = await GetAllAsync(); + if (filter != null) + { + ciphers = ciphers.Where(filter); + } + + foreach (var cipher in ciphers) + { + tasks.Add(decryptAndAddCipherAsync(cipher)); + } + await Task.WhenAll(tasks); + decCiphers = decCiphers.OrderBy(c => c, new CipherLocaleComparer(_i18nService)).ToList(); + + if (filter != null) + { + return decCiphers; + } + + DecryptedCipherCache = decCiphers; + return DecryptedCipherCache; + } + finally { } + } + _getAllDecryptedTask = doTask(); + return await _getAllDecryptedTask; + } + + public async Task> GetAllDecryptedForGroupingAsync(string groupingId, bool folder = true) + { + var ciphers = await GetAllDecryptedAsync(); + return ciphers.Where(cipher => + { + if (cipher.IsDeleted) + { + return false; + } + if (folder && cipher.FolderId == groupingId) + { + return true; + } + if (!folder && cipher.CollectionIds != null && cipher.CollectionIds.Contains(groupingId)) + { + return true; + } + return false; + }).ToList(); + } + + public async Task> GetAllDecryptedForUrlAsync(string url) + { + var all = await GetAllDecryptedByUrlAsync(url); + return all.Item1; + } + + public async Task, List, List>> GetAllDecryptedByUrlAsync( + string url, List includeOtherTypes = null) + { + if (string.IsNullOrWhiteSpace(url) && includeOtherTypes == null) + { + return new Tuple, List, List>( + new List(), new List(), new List()); + } + + var domain = CoreHelpers.GetDomain(url); + var mobileApp = UrlIsMobileApp(url); + + var mobileAppInfo = InfoFromMobileAppUrl(url); + var mobileAppWebUriString = mobileAppInfo?.Item1; + var mobileAppSearchTerms = mobileAppInfo?.Item2; + + var matchingDomainsTask = GetMatchingDomainsAsync(url, domain, mobileApp, mobileAppWebUriString); + var ciphersTask = GetAllDecryptedAsync(); + await Task.WhenAll(new List + { + matchingDomainsTask, + ciphersTask + }); + + var matchingDomains = await matchingDomainsTask; + var matchingDomainsSet = matchingDomains.Item1; + var matchingFuzzyDomainsSet = matchingDomains.Item2; + + var matchingLogins = new List(); + var matchingFuzzyLogins = new List(); + var others = new List(); + var ciphers = await ciphersTask; + + var defaultMatch = (UriMatchType?)(await _stateService.GetDefaultUriMatchAsync()); + if (defaultMatch == null) + { + defaultMatch = UriMatchType.Domain; + } + + foreach (var cipher in ciphers) + { + if (cipher.IsDeleted) + { + continue; + } + + if (cipher.Type != CipherType.Login && (includeOtherTypes?.Any(t => t == cipher.Type) ?? false)) + { + others.Add(cipher); + continue; + } + + if (cipher.Type != CipherType.Login || cipher.Login?.Uris == null || !cipher.Login.Uris.Any()) + { + continue; + } + + foreach (var u in cipher.Login.Uris) + { + if (string.IsNullOrWhiteSpace(u.Uri)) + { + continue; + } + var match = false; + var toMatch = defaultMatch; + if (u.Match != null) + { + toMatch = u.Match; + } + switch (toMatch) + { + case null: + case UriMatchType.Domain: + match = CheckDefaultUriMatch(cipher, u, matchingLogins, matchingFuzzyLogins, + matchingDomainsSet, matchingFuzzyDomainsSet, mobileApp, mobileAppSearchTerms); + if (match && u.Domain != null) + { + if (_domainMatchBlacklist.ContainsKey(u.Domain)) + { + var domainUrlHost = CoreHelpers.GetHost(url); + if (_domainMatchBlacklist[u.Domain].Contains(domainUrlHost)) + { + match = false; + } + } + } + break; + case UriMatchType.Host: + var urlHost = CoreHelpers.GetHost(url); + match = urlHost != null && urlHost == CoreHelpers.GetHost(u.Uri); + if (match) + { + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); + } + break; + case UriMatchType.Exact: + match = url == u.Uri; + if (match) + { + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); + } + break; + case UriMatchType.StartsWith: + match = url.StartsWith(u.Uri); + if (match) + { + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); + } + break; + case UriMatchType.RegularExpression: + try + { + var regex = new Regex(u.Uri, RegexOptions.IgnoreCase, TimeSpan.FromSeconds(1)); + match = regex.IsMatch(url); + if (match) + { + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); + } + } + catch (ArgumentException) { } + break; + case UriMatchType.Never: + default: + break; + } + if (match) + { + break; + } + } + } + return new Tuple, List, List>( + matchingLogins, matchingFuzzyLogins, others); + } + + public async Task GetLastUsedForUrlAsync(string url) + { + var ciphers = await GetAllDecryptedForUrlAsync(url); + return ciphers.OrderBy(c => c, new CipherLastUsedComparer()).FirstOrDefault(); + } + + public async Task UpdateLastUsedDateAsync(string id) + { + var ciphersLocalData = await _stateService.GetLocalDataAsync(); + if (ciphersLocalData == null) + { + ciphersLocalData = new Dictionary>(); + } + if (!ciphersLocalData.ContainsKey(id)) + { + ciphersLocalData.Add(id, new Dictionary()); + } + if (ciphersLocalData[id].ContainsKey("lastUsedDate")) + { + ciphersLocalData[id]["lastUsedDate"] = DateTime.UtcNow; + } + else + { + ciphersLocalData[id].Add("lastUsedDate", DateTime.UtcNow); + } + + await _stateService.SetLocalDataAsync(ciphersLocalData); + // Update cache + if (DecryptedCipherCache == null) + { + return; + } + var cached = DecryptedCipherCache.FirstOrDefault(c => c.Id == id); + if (cached != null) + { + cached.LocalData = ciphersLocalData[id]; + } + } + + public async Task SaveNeverDomainAsync(string domain) + { + if (string.IsNullOrWhiteSpace(domain)) + { + return; + } + var domains = await _stateService.GetNeverDomainsAsync(); + if (domains == null) + { + domains = new HashSet(); + } + domains.Add(domain); + await _stateService.SetNeverDomainsAsync(domains); + } + + public async Task SaveWithServerAsync(Cipher cipher) + { + CipherResponse response; + if (cipher.Id == null) + { + if (cipher.CollectionIds != null) + { + var request = new CipherCreateRequest(cipher); + response = await _apiService.PostCipherCreateAsync(request); + } + else + { + var request = new CipherRequest(cipher); + response = await _apiService.PostCipherAsync(request); + } + cipher.Id = response.Id; + } + else + { + var request = new CipherRequest(cipher); + response = await _apiService.PutCipherAsync(cipher.Id, request); + } + var userId = await _stateService.GetActiveUserIdAsync(); + var data = new CipherData(response, userId, cipher.CollectionIds); + await UpsertAsync(data); + } + + public async Task ShareWithServerAsync(CipherView cipher, string organizationId, HashSet collectionIds) + { + if (!await ValidateCanBeSharedWithOrgAsync(cipher, organizationId)) + { + return ICipherService.ShareWithServerError.DuplicatedPasskeyInOrg; + } + + var attachmentTasks = new List(); + if (cipher.Attachments != null) + { + foreach (var attachment in cipher.Attachments) + { + if (attachment.Key == null) + { + attachmentTasks.Add(ShareAttachmentWithServerAsync(attachment, cipher.Id, organizationId)); + } + } + } + await Task.WhenAll(attachmentTasks); + cipher.OrganizationId = organizationId; + cipher.CollectionIds = collectionIds; + var encCipher = await EncryptAsync(cipher); + var request = new CipherShareRequest(encCipher); + var response = await _apiService.PutShareCipherAsync(cipher.Id, request); + var userId = await _stateService.GetActiveUserIdAsync(); + var data = new CipherData(response, userId, collectionIds); + await UpsertAsync(data); + + return ICipherService.ShareWithServerError.None; + } + + private async Task ValidateCanBeSharedWithOrgAsync(CipherView cipher, string organizationId) + { + if (cipher.Login?.Fido2Key is null && cipher.Fido2Key is null) + { + return true; + } + + var decCiphers = await GetAllDecryptedAsync(); + var orgCiphers = decCiphers.Where(c => c.OrganizationId == organizationId); + if (cipher.Login?.Fido2Key != null) + { + return !orgCiphers.Any(c => !cipher.Login.Fido2Key.IsUniqueAgainst(c.Login?.Fido2Key) + || + !cipher.Login.Fido2Key.IsUniqueAgainst(c.Fido2Key)); + } + + if (cipher.Fido2Key != null) + { + return !orgCiphers.Any(c => !cipher.Fido2Key.IsUniqueAgainst(c.Login?.Fido2Key) + || + !cipher.Fido2Key.IsUniqueAgainst(c.Fido2Key)); + } + + return true; + } + + public async Task SaveAttachmentRawWithServerAsync(Cipher cipher, string filename, byte[] data) + { + var orgKey = await _cryptoService.GetOrgKeyAsync(cipher.OrganizationId); + var encFileName = await _cryptoService.EncryptAsync(filename, orgKey); + var (attachmentKey, orgEncAttachmentKey) = await _cryptoService.MakeEncKeyAsync(orgKey); + var encFileData = await _cryptoService.EncryptToBytesAsync(data, attachmentKey); + + CipherResponse response; + try + { + var request = new AttachmentRequest + { + Key = orgEncAttachmentKey.EncryptedString, + FileName = encFileName.EncryptedString, + FileSize = encFileData.Buffer.Length, + }; + + var uploadDataResponse = await _apiService.PostCipherAttachmentAsync(cipher.Id, request); + response = uploadDataResponse.CipherResponse; + await _fileUploadService.UploadCipherAttachmentFileAsync(uploadDataResponse, encFileName, encFileData); + } + catch (ApiException e) when (e.Error.StatusCode == System.Net.HttpStatusCode.NotFound || e.Error.StatusCode == System.Net.HttpStatusCode.MethodNotAllowed) + { + response = await LegacyServerAttachmentFileUploadAsync(cipher.Id, encFileName, encFileData, orgEncAttachmentKey); + } + + var userId = await _stateService.GetActiveUserIdAsync(); + var cData = new CipherData(response, userId, cipher.CollectionIds); + await UpsertAsync(cData); + return new Cipher(cData); + } + + [Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")] + private async Task LegacyServerAttachmentFileUploadAsync(string cipherId, + EncString encFileName, EncByteArray encFileData, EncString key) + { + var boundary = string.Concat("--BWMobileFormBoundary", DateTime.UtcNow.Ticks); + var fd = new MultipartFormDataContent(boundary); + fd.Add(new StringContent(key.EncryptedString), "key"); + fd.Add(new StreamContent(new MemoryStream(encFileData.Buffer)), "data", encFileName.EncryptedString); + return await _apiService.PostCipherAttachmentLegacyAsync(cipherId, fd); + } + + public async Task SaveCollectionsWithServerAsync(Cipher cipher) + { + var request = new CipherCollectionsRequest(cipher.CollectionIds?.ToList()); + await _apiService.PutCipherCollectionsAsync(cipher.Id, request); + var userId = await _stateService.GetActiveUserIdAsync(); + var data = cipher.ToCipherData(userId); + await UpsertAsync(data); + } + + public async Task UpsertAsync(CipherData cipher) + { + var ciphers = await _stateService.GetEncryptedCiphersAsync(); + if (ciphers == null) + { + ciphers = new Dictionary(); + } + if (!ciphers.ContainsKey(cipher.Id)) + { + ciphers.Add(cipher.Id, null); + } + ciphers[cipher.Id] = cipher; + await _stateService.SetEncryptedCiphersAsync(ciphers); + await ClearCacheAsync(); + } + + public async Task UpsertAsync(List cipher) + { + var ciphers = await _stateService.GetEncryptedCiphersAsync(); + if (ciphers == null) + { + ciphers = new Dictionary(); + } + foreach (var c in cipher) + { + if (!ciphers.ContainsKey(c.Id)) + { + ciphers.Add(c.Id, null); + } + ciphers[c.Id] = c; + } + await _stateService.SetEncryptedCiphersAsync(ciphers); + await ClearCacheAsync(); + } + + public async Task ReplaceAsync(Dictionary ciphers) + { + await _stateService.SetEncryptedCiphersAsync(ciphers); + await ClearCacheAsync(); + } + + public async Task ClearAsync(string userId) + { + await _stateService.SetEncryptedCiphersAsync(null, userId); + await ClearCacheAsync(); + } + + public async Task DeleteAsync(string id) + { + var ciphers = await _stateService.GetEncryptedCiphersAsync(); + if (ciphers == null) + { + return; + } + if (!ciphers.ContainsKey(id)) + { + return; + } + ciphers.Remove(id); + await _stateService.SetEncryptedCiphersAsync(ciphers); + await ClearCacheAsync(); + } + + public async Task DeleteAsync(List ids) + { + var ciphers = await _stateService.GetEncryptedCiphersAsync(); + if (ciphers == null) + { + return; + } + foreach (var id in ids) + { + if (!ciphers.ContainsKey(id)) + { + return; + } + ciphers.Remove(id); + } + await _stateService.SetEncryptedCiphersAsync(ciphers); + await ClearCacheAsync(); + } + + public async Task DeleteWithServerAsync(string id) + { + await _apiService.DeleteCipherAsync(id); + await DeleteAsync(id); + } + + public async Task DeleteAttachmentAsync(string id, string attachmentId) + { + var ciphers = await _stateService.GetEncryptedCiphersAsync(); + if (ciphers == null || !ciphers.ContainsKey(id) || ciphers[id].Attachments == null) + { + return; + } + var attachment = ciphers[id].Attachments.FirstOrDefault(a => a.Id == attachmentId); + if (attachment != null) + { + ciphers[id].Attachments.Remove(attachment); + } + await _stateService.SetEncryptedCiphersAsync(ciphers); + await ClearCacheAsync(); + } + + public async Task DeleteAttachmentWithServerAsync(string id, string attachmentId) + { + try + { + await _apiService.DeleteCipherAttachmentAsync(id, attachmentId); + await DeleteAttachmentAsync(id, attachmentId); + } + catch (ApiException e) + { + await DeleteAttachmentAsync(id, attachmentId); + throw; + } + } + + public async Task DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId) + { + string url; + try + { + var attachmentDownloadResponse = await _apiService.GetAttachmentData(cipherId, attachment.Id); + url = attachmentDownloadResponse.Url; + } + // TODO: Delete this catch when all Servers are updated to respond to the above method + catch (ApiException e) when (e.Error.StatusCode == System.Net.HttpStatusCode.NotFound) + { + url = attachment.Url; + } + + try + { + var response = await _httpClient.GetAsync(new Uri(url)); + if (!response.IsSuccessStatusCode) + { + return null; + } + var data = await response.Content.ReadAsByteArrayAsync(); + if (data == null) + { + return null; + } + var key = attachment.Key ?? await _cryptoService.GetOrgKeyAsync(organizationId); + return await _cryptoService.DecryptFromBytesAsync(data, key); + } + catch { } + return null; + } + + public async Task SoftDeleteWithServerAsync(string id) + { + var ciphers = await _stateService.GetEncryptedCiphersAsync(); + if (ciphers == null) + { + return; + } + if (!ciphers.ContainsKey(id)) + { + return; + } + + await _apiService.PutDeleteCipherAsync(id); + ciphers[id].DeletedDate = DateTime.UtcNow; + await _stateService.SetEncryptedCiphersAsync(ciphers); + await ClearCacheAsync(); + } + + public async Task RestoreWithServerAsync(string id) + { + var ciphers = await _stateService.GetEncryptedCiphersAsync(); + if (ciphers == null) + { + return; + } + if (!ciphers.ContainsKey(id)) + { + return; + } + var response = await _apiService.PutRestoreCipherAsync(id); + ciphers[id].DeletedDate = null; + ciphers[id].RevisionDate = response.RevisionDate; + await _stateService.SetEncryptedCiphersAsync(ciphers); + await ClearCacheAsync(); + } + + // Helpers + + private async Task ShareAttachmentWithServerAsync(AttachmentView attachmentView, string cipherId, + string organizationId) + { + var attachmentResponse = await _httpClient.GetAsync(attachmentView.Url); + if (!attachmentResponse.IsSuccessStatusCode) + { + throw new Exception("Failed to download attachment: " + attachmentResponse.StatusCode); + } + + var bytes = await attachmentResponse.Content.ReadAsByteArrayAsync(); + var decBytes = await _cryptoService.DecryptFromBytesAsync(bytes, null); + var key = await _cryptoService.GetOrgKeyAsync(organizationId); + var encFileName = await _cryptoService.EncryptAsync(attachmentView.FileName, key); + var dataEncKey = await _cryptoService.MakeEncKeyAsync(key); + var encData = await _cryptoService.EncryptToBytesAsync(decBytes, dataEncKey.Item1); + var boundary = string.Concat("--BWMobileFormBoundary", DateTime.UtcNow.Ticks); + var fd = new MultipartFormDataContent(boundary); + fd.Add(new StringContent(dataEncKey.Item2.EncryptedString), "key"); + fd.Add(new StreamContent(new MemoryStream(encData.Buffer)), "data", encFileName.EncryptedString); + await _apiService.PostShareCipherAttachmentAsync(cipherId, attachmentView.Id, fd, organizationId); + } + + private bool CheckDefaultUriMatch(CipherView cipher, LoginUriView loginUri, + List matchingLogins, List matchingFuzzyLogins, + HashSet matchingDomainsSet, HashSet matchingFuzzyDomainsSet, + bool mobileApp, string[] mobileAppSearchTerms) + { + var loginUriString = loginUri.Uri; + var loginUriDomain = loginUri.Domain; + + if (matchingDomainsSet.Contains(loginUriString)) + { + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); + return true; + } + else if (mobileApp && matchingFuzzyDomainsSet.Contains(loginUriString)) + { + AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); + return false; + } + else if (!mobileApp) + { + var info = InfoFromMobileAppUrl(loginUriString); + if (info?.Item1 != null && matchingDomainsSet.Contains(info.Item1)) + { + AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); + return false; + } + } + + if (!loginUri.Uri.Contains("://") && loginUriString.Contains(".")) + { + loginUriString = "http://" + loginUriString; + } + + if (loginUriDomain != null) + { + loginUriDomain = loginUriDomain.ToLowerInvariant(); + if (matchingDomainsSet.Contains(loginUriDomain)) + { + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); + return true; + } + else if (mobileApp && matchingFuzzyDomainsSet.Contains(loginUriDomain)) + { + AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); + return false; + } + } + + if (mobileApp && (mobileAppSearchTerms?.Any() ?? false)) + { + var addedFromSearchTerm = false; + var loginName = cipher.Name?.ToLowerInvariant(); + foreach (var term in mobileAppSearchTerms) + { + addedFromSearchTerm = (loginUriDomain != null && loginUriDomain.Contains(term)) || + (loginName != null && loginName.Contains(term)); + if (!addedFromSearchTerm) + { + var domainTerm = loginUriDomain?.Split('.')[0]; + addedFromSearchTerm = + (domainTerm != null && domainTerm.Length > 2 && term.Contains(domainTerm)) || + (loginName != null && term.Contains(loginName)); + } + if (addedFromSearchTerm) + { + AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); + return false; + } + } + } + + return false; + } + + private void AddMatchingLogin(CipherView cipher, List matchingLogins, + List matchingFuzzyLogins) + { + if (matchingFuzzyLogins.Contains(cipher)) + { + matchingFuzzyLogins.Remove(cipher); + } + matchingLogins.Add(cipher); + } + + private void AddMatchingFuzzyLogin(CipherView cipher, List matchingLogins, + List matchingFuzzyLogins) + { + if (!matchingFuzzyLogins.Contains(cipher) && !matchingLogins.Contains(cipher)) + { + matchingFuzzyLogins.Add(cipher); + } + } + + private async Task, HashSet>> GetMatchingDomainsAsync(string url, + string domain, bool mobileApp, string mobileAppWebUriString) + { + var matchingDomains = new HashSet(); + var matchingFuzzyDomains = new HashSet(); + var eqDomains = await _settingsService.GetEquivalentDomainsAsync(); + foreach (var eqDomain in eqDomains) + { + var eqDomainSet = new HashSet(eqDomain); + if (mobileApp) + { + if (eqDomainSet.Contains(url)) + { + foreach (var d in eqDomain) + { + matchingDomains.Add(d); + } + } + else if (mobileAppWebUriString != null && eqDomainSet.Contains(mobileAppWebUriString)) + { + foreach (var d in eqDomain) + { + matchingFuzzyDomains.Add(d); + } + } + } + else if (eqDomainSet.Contains(domain)) + { + foreach (var d in eqDomain) + { + matchingDomains.Add(d); + } + } + } + if (!matchingDomains.Any()) + { + matchingDomains.Add(mobileApp ? url : domain); + } + if (mobileApp && mobileAppWebUriString != null && + !matchingFuzzyDomains.Any() && !matchingDomains.Contains(mobileAppWebUriString)) + { + matchingFuzzyDomains.Add(mobileAppWebUriString); + } + return new Tuple, HashSet>(matchingDomains, matchingFuzzyDomains); + } + + private Tuple InfoFromMobileAppUrl(string mobileAppUrlString) + { + if (UrlIsAndroidApp(mobileAppUrlString)) + { + return InfoFromAndroidAppUri(mobileAppUrlString); + } + else if (UrlIsiOSApp(mobileAppUrlString)) + { + return InfoFromiOSAppUrl(mobileAppUrlString); + } + return null; + } + + private Tuple InfoFromAndroidAppUri(string androidAppUrlString) + { + if (!UrlIsAndroidApp(androidAppUrlString)) + { + return null; + } + var androidUrlParts = androidAppUrlString.Replace(Constants.AndroidAppProtocol, string.Empty).Split('.'); + if (androidUrlParts.Length >= 2) + { + var webUri = string.Join(".", androidUrlParts[1], androidUrlParts[0]); + var searchTerms = androidUrlParts.Where(p => !_ignoredSearchTerms.Contains(p)) + .Select(p => p.ToLowerInvariant()).ToArray(); + return new Tuple(webUri, searchTerms); + } + return null; + } + + private Tuple InfoFromiOSAppUrl(string iosAppUrlString) + { + if (!UrlIsiOSApp(iosAppUrlString)) + { + return null; + } + var webUri = iosAppUrlString.Replace(Constants.iOSAppProtocol, string.Empty); + return new Tuple(webUri, null); + } + + private bool UrlIsMobileApp(string url) + { + return UrlIsAndroidApp(url) || UrlIsiOSApp(url); + } + + private bool UrlIsAndroidApp(string url) + { + return url.StartsWith(Constants.AndroidAppProtocol); + } + + private bool UrlIsiOSApp(string url) + { + return url.StartsWith(Constants.iOSAppProtocol); + } + + private Task EncryptObjPropertyAsync(V model, D obj, HashSet map, SymmetricCryptoKey key) + where V : View + where D : Domain + { + var modelType = model.GetType(); + var objType = obj.GetType(); + + async Task makeAndSetCs(string propName) + { + var modelPropInfo = modelType.GetProperty(propName); + var modelProp = modelPropInfo.GetValue(model) as string; + EncString val = null; + if (!string.IsNullOrWhiteSpace(modelProp)) + { + val = await _cryptoService.EncryptAsync(modelProp, key); + } + var objPropInfo = objType.GetProperty(propName); + objPropInfo.SetValue(obj, val, null); + }; + + var tasks = new List(); + foreach (var prop in map) + { + tasks.Add(makeAndSetCs(prop)); + } + return Task.WhenAll(tasks); + } + + private async Task EncryptAttachmentsAsync(List attachmentsModel, SymmetricCryptoKey key, + Cipher cipher) + { + if (!attachmentsModel?.Any() ?? true) + { + cipher.Attachments = null; + return; + } + var tasks = new List(); + var encAttachments = new List(); + async Task encryptAndAddAttachmentAsync(AttachmentView model, Attachment attachment) + { + await EncryptObjPropertyAsync(model, attachment, new HashSet + { + "FileName" + }, key); + if (model.Key != null) + { + attachment.Key = await _cryptoService.EncryptAsync(model.Key.Key, key); + } + encAttachments.Add(attachment); + } + foreach (var model in attachmentsModel) + { + tasks.Add(encryptAndAddAttachmentAsync(model, new Attachment + { + Id = model.Id, + Size = model.Size, + SizeName = model.SizeName, + Url = model.Url + })); + } + await Task.WhenAll(tasks); + cipher.Attachments = encAttachments; + } + + private async Task EncryptCipherDataAsync(Cipher cipher, CipherView model, SymmetricCryptoKey key) + { + switch (cipher.Type) + { + case CipherType.Login: + cipher.Login = new Login + { + PasswordRevisionDate = model.Login.PasswordRevisionDate + }; + await EncryptObjPropertyAsync(model.Login, cipher.Login, new HashSet + { + "Username", + "Password", + "Totp" + }, key); + if (model.Login.Uris != null) + { + cipher.Login.Uris = new List(); + foreach (var uri in model.Login.Uris) + { + var loginUri = new LoginUri + { + Match = uri.Match + }; + await EncryptObjPropertyAsync(uri, loginUri, new HashSet { "Uri" }, key); + cipher.Login.Uris.Add(loginUri); + } + } + if (model.Login.Fido2Key != null) + { + cipher.Login.Fido2Key = new Fido2Key(); + await EncryptObjPropertyAsync(model.Login.Fido2Key, cipher.Login.Fido2Key, Fido2Key.EncryptableProperties, key); + } + break; + case CipherType.SecureNote: + cipher.SecureNote = new SecureNote + { + Type = model.SecureNote.Type + }; + break; + case CipherType.Card: + cipher.Card = new Card(); + await EncryptObjPropertyAsync(model.Card, cipher.Card, new HashSet + { + "CardholderName", + "Brand", + "Number", + "ExpMonth", + "ExpYear", + "Code" + }, key); + break; + case CipherType.Identity: + cipher.Identity = new Identity(); + await EncryptObjPropertyAsync(model.Identity, cipher.Identity, new HashSet + { + "Title", + "FirstName", + "MiddleName", + "LastName", + "Address1", + "Address2", + "Address3", + "City", + "State", + "PostalCode", + "Country", + "Company", + "Email", + "Phone", + "SSN", + "Username", + "PassportNumber", + "LicenseNumber" + }, key); + break; + case CipherType.Fido2Key: + cipher.Fido2Key = new Fido2Key(); + await EncryptObjPropertyAsync(model.Fido2Key, cipher.Fido2Key, Fido2Key.EncryptableProperties, key); + break; + default: + throw new Exception("Unknown cipher type."); + } + } + + private async Task EncryptFieldsAsync(List fieldsModel, SymmetricCryptoKey key, + Cipher cipher) + { + if (!fieldsModel?.Any() ?? true) + { + cipher.Fields = null; + return; + } + var tasks = new List(); + var encFields = new List(); + async Task encryptAndAddFieldAsync(FieldView model, Field field) + { + await EncryptObjPropertyAsync(model, field, new HashSet + { + "Name", + "Value" + }, key); + encFields.Add(field); + } + foreach (var model in fieldsModel) + { + var field = new Field + { + Type = model.Type, + LinkedId = model.LinkedId, + }; + // normalize boolean type field values + if (model.Type == FieldType.Boolean && model.Value != "true") + { + model.Value = "false"; + } + tasks.Add(encryptAndAddFieldAsync(model, field)); + } + await Task.WhenAll(tasks); + cipher.Fields = encFields; + } + + private async Task EncryptPasswordHistoriesAsync(List phModels, + SymmetricCryptoKey key, Cipher cipher) + { + if (!phModels?.Any() ?? true) + { + cipher.PasswordHistory = null; + return; + } + var tasks = new List(); + var encPhs = new List(); + async Task encryptAndAddHistoryAsync(PasswordHistoryView model, PasswordHistory ph) + { + await EncryptObjPropertyAsync(model, ph, new HashSet + { + "Password" + }, key); + encPhs.Add(ph); + } + foreach (var model in phModels) + { + tasks.Add(encryptAndAddHistoryAsync(model, new PasswordHistory + { + LastUsedDate = model.LastUsedDate + })); + } + await Task.WhenAll(tasks); + cipher.PasswordHistory = encPhs; + } + + private class CipherLocaleComparer : IComparer + { + private readonly II18nService _i18nService; + + public CipherLocaleComparer(II18nService i18nService) + { + _i18nService = i18nService; + } + + public int Compare(CipherView a, CipherView b) + { + var aName = a?.ComparableName; + var bName = b?.ComparableName; + if (aName == null && bName != null) + { + return -1; + } + if (aName != null && bName == null) + { + return 1; + } + if (aName == null && bName == null) + { + return 0; + } + return _i18nService.StringComparer.Compare(aName, bName); + } + } + + private class CipherLastUsedComparer : IComparer + { + public int Compare(CipherView a, CipherView b) + { + var aLastUsed = a.LocalData != null && a.LocalData.ContainsKey("lastUsedDate") ? + a.LocalData["lastUsedDate"] as DateTime? : null; + var bLastUsed = b.LocalData != null && b.LocalData.ContainsKey("lastUsedDate") ? + b.LocalData["lastUsedDate"] as DateTime? : null; + + var bothNotNull = aLastUsed != null && bLastUsed != null; + if (bothNotNull && aLastUsed.Value < bLastUsed.Value) + { + return 1; + } + if (aLastUsed != null && bLastUsed == null) + { + return -1; + } + if (bothNotNull && aLastUsed.Value > bLastUsed.Value) + { + return -1; + } + if (bLastUsed != null && aLastUsed == null) + { + return 1; + } + return 0; + } + } + + private class CipherLastUsedThenNameComparer : IComparer + { + private CipherLastUsedComparer _cipherLastUsedComparer; + private CipherLocaleComparer _cipherLocaleComparer; + + public CipherLastUsedThenNameComparer(II18nService i18nService) + { + _cipherLastUsedComparer = new CipherLastUsedComparer(); + _cipherLocaleComparer = new CipherLocaleComparer(i18nService); + } + + public int Compare(CipherView a, CipherView b) + { + var result = _cipherLastUsedComparer.Compare(a, b); + if (result != 0) + { + return result; + } + return _cipherLocaleComparer.Compare(a, b); + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/CollectionService.cs b/src/Maui/Bitwarden/Core/Services/CollectionService.cs new file mode 100644 index 000000000..fe30159dc --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/CollectionService.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; +using Bit.Core.Utilities; + +namespace Bit.Core.Services +{ + public class CollectionService : ICollectionService + { + private const char NestingDelimiter = '/'; + + private List _decryptedCollectionCache; + private readonly ICryptoService _cryptoService; + private readonly IStateService _stateService; + private readonly II18nService _i18nService; + + public CollectionService( + ICryptoService cryptoService, + IStateService stateService, + II18nService i18nService) + { + _cryptoService = cryptoService; + _stateService = stateService; + _i18nService = i18nService; + } + + public void ClearCache() + { + _decryptedCollectionCache = null; + } + + public async Task EncryptAsync(CollectionView model) + { + if (model.OrganizationId == null) + { + throw new Exception("Collection has no organization id."); + } + var key = await _cryptoService.GetOrgKeyAsync(model.OrganizationId); + if (key == null) + { + throw new Exception("No key for this collection's organization."); + } + var collection = new Collection + { + Id = model.Id, + OrganizationId = model.OrganizationId, + ReadOnly = model.ReadOnly, + Name = await _cryptoService.EncryptAsync(model.Name, key) + }; + return collection; + } + + public async Task> DecryptManyAsync(List collections) + { + if (collections == null) + { + return new List(); + } + var decCollections = new List(); + async Task decryptAndAddCollectionAsync(Collection collection) + { + var c = await collection.DecryptAsync(); + decCollections.Add(c); + } + var tasks = new List(); + foreach (var collection in collections) + { + tasks.Add(decryptAndAddCollectionAsync(collection)); + } + await Task.WhenAll(tasks); + return decCollections.OrderBy(c => c, new CollectionLocaleComparer(_i18nService)).ToList(); + } + + public async Task GetAsync(string id) + { + var collections = await _stateService.GetEncryptedCollectionsAsync(); + if (!collections?.ContainsKey(id) ?? true) + { + return null; + } + return new Collection(collections[id]); + } + + public async Task> GetAllAsync() + { + var collections = await _stateService.GetEncryptedCollectionsAsync(); + var response = collections?.Select(c => new Collection(c.Value)); + return response?.ToList() ?? new List(); + } + + // TODO: sequentialize? + public async Task> GetAllDecryptedAsync() + { + if (_decryptedCollectionCache != null) + { + return _decryptedCollectionCache; + } + var hasKey = await _cryptoService.HasKeyAsync(); + if (!hasKey) + { + throw new Exception("No key."); + } + var collections = await GetAllAsync(); + _decryptedCollectionCache = await DecryptManyAsync(collections); + return _decryptedCollectionCache; + } + + public async Task>> GetAllNestedAsync(List collections = null) + { + if (collections == null) + { + collections = await GetAllDecryptedAsync(); + } + var nodes = new List>(); + foreach (var c in collections) + { + var collectionCopy = new CollectionView + { + Id = c.Id, + OrganizationId = c.OrganizationId + }; + var parts = c.Name != null ? + Regex.Replace(c.Name, "^\\/+|\\/+$", string.Empty).Split(NestingDelimiter) : new string[] { }; + CoreHelpers.NestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter); + } + return nodes; + } + + public async Task> GetNestedAsync(string id) + { + var collections = await GetAllNestedAsync(); + return CoreHelpers.GetTreeNodeObject(collections, id); + } + + public async Task UpsertAsync(CollectionData collection) + { + var collections = await _stateService.GetEncryptedCollectionsAsync(); + if (collections == null) + { + collections = new Dictionary(); + } + if (!collections.ContainsKey(collection.Id)) + { + collections.Add(collection.Id, null); + } + collections[collection.Id] = collection; + await _stateService.SetEncryptedCollectionsAsync(collections); + _decryptedCollectionCache = null; + } + + public async Task UpsertAsync(List collection) + { + var collections = await _stateService.GetEncryptedCollectionsAsync(); + if (collections == null) + { + collections = new Dictionary(); + } + foreach (var c in collection) + { + if (!collections.ContainsKey(c.Id)) + { + collections.Add(c.Id, null); + } + collections[c.Id] = c; + } + await _stateService.SetEncryptedCollectionsAsync(collections); + _decryptedCollectionCache = null; + } + + public async Task ReplaceAsync(Dictionary collections) + { + await _stateService.SetEncryptedCollectionsAsync(collections); + _decryptedCollectionCache = null; + } + + public async Task ClearAsync(string userId) + { + await _stateService.SetEncryptedCollectionsAsync(null, userId); + _decryptedCollectionCache = null; + } + + public async Task DeleteAsync(string id) + { + var collections = await _stateService.GetEncryptedCollectionsAsync(); + if (collections == null || !collections.ContainsKey(id)) + { + return; + } + collections.Remove(id); + await _stateService.SetEncryptedCollectionsAsync(collections); + _decryptedCollectionCache = null; + } + + private class CollectionLocaleComparer : IComparer + { + private readonly II18nService _i18nService; + + public CollectionLocaleComparer(II18nService i18nService) + { + _i18nService = i18nService; + } + + public int Compare(CollectionView a, CollectionView b) + { + var aName = a?.Name; + var bName = b?.Name; + if (aName == null && bName != null) + { + return -1; + } + if (aName != null && bName == null) + { + return 1; + } + if (aName == null && bName == null) + { + return 0; + } + return _i18nService.StringComparer.Compare(aName, bName); + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/ConditionedAwaiterManager.cs b/src/Maui/Bitwarden/Core/Services/ConditionedAwaiterManager.cs new file mode 100644 index 000000000..f318996f5 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/ConditionedAwaiterManager.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Abstractions; + +namespace Bit.Core.Services +{ + public class ConditionedAwaiterManager : IConditionedAwaiterManager + { + private readonly ConcurrentDictionary> _preconditionsTasks = new ConcurrentDictionary> + { + [AwaiterPrecondition.EnvironmentUrlsInited] = new TaskCompletionSource() + }; + + public Task GetAwaiterForPrecondition(AwaiterPrecondition awaiterPrecondition) + { + if (_preconditionsTasks.TryGetValue(awaiterPrecondition, out var tcs)) + { + return tcs.Task; + } + + return Task.CompletedTask; + } + + public void SetAsCompleted(AwaiterPrecondition awaiterPrecondition) + { + if (_preconditionsTasks.TryGetValue(awaiterPrecondition, out var tcs)) + { + tcs.TrySetResult(true); + } + } + + public void SetException(AwaiterPrecondition awaiterPrecondition, Exception ex) + { + if (_preconditionsTasks.TryGetValue(awaiterPrecondition, out var tcs)) + { + tcs.TrySetException(ex); + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/ConfigService.cs b/src/Maui/Bitwarden/Core/Services/ConfigService.cs new file mode 100644 index 000000000..360fc4b0f --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/ConfigService.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Response; +using Bit.Core.Models.View; + +namespace Bit.Core.Services +{ + public class ConfigService : IConfigService + { + private const int UPDATE_INTERVAL_MINS = 60; + private ConfigResponse _configs; + private readonly IApiService _apiService; + private readonly IStateService _stateService; + private readonly ILogger _logger; + + public ConfigService(IApiService apiService, IStateService stateService, ILogger logger) + { + _apiService = apiService; + _stateService = stateService; + _logger = logger; + } + + public async Task GetAsync(bool forceRefresh = false) + { + try + { + _configs = _stateService.GetConfigs(); + if (forceRefresh || _configs?.ExpiresOn is null || _configs.ExpiresOn <= DateTime.UtcNow) + { + _configs = await _apiService.GetConfigsAsync(); + _configs.ExpiresOn = DateTime.UtcNow.AddMinutes(UPDATE_INTERVAL_MINS); + _stateService.SetConfigs(_configs); + } + } + catch (ApiException ex) when (ex.Error.StatusCode == System.Net.HttpStatusCode.BadGateway) + { + // ignore if there is no internet connection and return local configs + } + catch (Exception ex) + { + _logger.Exception(ex); + } + + return _configs; + } + + public async Task GetFeatureFlagBoolAsync(string key, bool forceRefresh = false, bool defaultValue = false) => await GetFeatureFlagAsync(key, forceRefresh, defaultValue); + + public async Task GetFeatureFlagStringAsync(string key, bool forceRefresh = false, string defaultValue = null) => await GetFeatureFlagAsync(key, forceRefresh, defaultValue); + + public async Task GetFeatureFlagIntAsync(string key, bool forceRefresh = false, int defaultValue = 0) => await GetFeatureFlagAsync(key, forceRefresh, defaultValue); + + private async Task GetFeatureFlagAsync(string key, bool forceRefresh = false, T defaultValue = default) + { + await GetAsync(forceRefresh); + if (_configs == null || _configs.FeatureStates == null) + { + return defaultValue; + } + + if (_configs.FeatureStates.TryGetValue(key, out var val) == true + && + val is T actualValue) + { + return actualValue; + } + + return defaultValue; + } + } +} + diff --git a/src/Maui/Bitwarden/Core/Services/ConsoleLogService.cs b/src/Maui/Bitwarden/Core/Services/ConsoleLogService.cs new file mode 100644 index 000000000..1814d48c6 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/ConsoleLogService.cs @@ -0,0 +1,28 @@ +using System; +using Bit.Core.Abstractions; + +namespace Bit.Core.Services +{ + public class ConsoleLogService : INativeLogService + { + public void Debug(string message) + { + Console.WriteLine("DEBUG: {0}", message); + } + + public void Info(string message) + { + Console.WriteLine("INFO: {0}", message); + } + + public void Warning(string message) + { + Console.WriteLine("WARNING: {0}", message); + } + + public void Error(string message) + { + Console.WriteLine("ERROR: {0}", message); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/CryptoService.cs b/src/Maui/Bitwarden/Core/Services/CryptoService.cs new file mode 100644 index 000000000..bfa7c5b82 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/CryptoService.cs @@ -0,0 +1,913 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Response; +using Bit.Core.Utilities; + +namespace Bit.Core.Services +{ + public class CryptoService : ICryptoService + { + private const string RANDOM_STRING_CHARSET = "abcdefghijklmnopqrstuvwxyz1234567890"; + + private readonly IStateService _stateService; + private readonly ICryptoFunctionService _cryptoFunctionService; + + private SymmetricCryptoKey _encKey; + private SymmetricCryptoKey _legacyEtmKey; + private string _keyHash; + private byte[] _publicKey; + private byte[] _privateKey; + private Dictionary _orgKeys; + private Task _getEncKeysTask; + private Task> _getOrgKeysTask; + + public CryptoService( + IStateService stateService, + ICryptoFunctionService cryptoFunctionService) + { + _stateService = stateService; + _cryptoFunctionService = cryptoFunctionService; + } + + public async Task SetKeyAsync(SymmetricCryptoKey key) + { + await _stateService.SetKeyDecryptedAsync(key); + var option = await _stateService.GetVaultTimeoutAsync(); + var biometric = await _stateService.GetBiometricUnlockAsync(); + if (option.HasValue && !biometric.GetValueOrDefault()) + { + // If we have a lock option set, we do not store the key + return; + } + await _stateService.SetKeyEncryptedAsync(key?.KeyB64); + } + + public async Task SetKeyHashAsync(string keyHash) + { + _keyHash = keyHash; + await _stateService.SetKeyHashAsync(keyHash); + } + + public async Task SetEncKeyAsync(string encKey) + { + if (encKey == null) + { + return; + } + await _stateService.SetEncKeyEncryptedAsync(encKey); + _encKey = null; + } + + public async Task SetEncPrivateKeyAsync(string encPrivateKey) + { + if (encPrivateKey == null) + { + return; + } + await _stateService.SetPrivateKeyEncryptedAsync(encPrivateKey); + _privateKey = null; + } + + public async Task SetOrgKeysAsync(IEnumerable orgs) + { + var orgKeys = orgs.ToDictionary(org => org.Id, org => org.Key); + _orgKeys = null; + await _stateService.SetOrgKeysEncryptedAsync(orgKeys); + } + + public async Task GetKeyAsync(string userId = null) + { + var inMemoryKey = await _stateService.GetKeyDecryptedAsync(userId); + if (inMemoryKey != null) + { + return inMemoryKey; + } + var key = await _stateService.GetKeyEncryptedAsync(userId); + if (key != null) + { + inMemoryKey = new SymmetricCryptoKey(Convert.FromBase64String(key)); + await _stateService.SetKeyDecryptedAsync(inMemoryKey, userId); + } + return inMemoryKey; + } + + public async Task GetKeyHashAsync() + { + if (_keyHash != null) + { + return _keyHash; + } + var keyHash = await _stateService.GetKeyHashAsync(); + if (keyHash != null) + { + _keyHash = keyHash; + } + return _keyHash; + } + + public Task GetEncKeyAsync(SymmetricCryptoKey key = null) + { + if (_encKey != null) + { + return Task.FromResult(_encKey); + } + if (_getEncKeysTask != null && !_getEncKeysTask.IsCompleted && !_getEncKeysTask.IsFaulted) + { + return _getEncKeysTask; + } + async Task doTask() + { + try + { + var encKey = await _stateService.GetEncKeyEncryptedAsync(); + if (encKey == null) + { + return null; + } + + if (key == null) + { + key = await GetKeyAsync(); + } + if (key == null) + { + return null; + } + + byte[] decEncKey = null; + var encKeyCipher = new EncString(encKey); + if (encKeyCipher.EncryptionType == EncryptionType.AesCbc256_B64) + { + decEncKey = await DecryptToBytesAsync(encKeyCipher, key); + } + else if (encKeyCipher.EncryptionType == EncryptionType.AesCbc256_HmacSha256_B64) + { + var newKey = await StretchKeyAsync(key); + decEncKey = await DecryptToBytesAsync(encKeyCipher, newKey); + } + else + { + throw new Exception("Unsupported encKey type."); + } + + if (decEncKey == null) + { + return null; + } + _encKey = new SymmetricCryptoKey(decEncKey); + return _encKey; + } + finally + { + _getEncKeysTask = null; + } + } + _getEncKeysTask = doTask(); + return _getEncKeysTask; + } + + public async Task GetPublicKeyAsync() + { + if (_publicKey != null) + { + return _publicKey; + } + var privateKey = await GetPrivateKeyAsync(); + if (privateKey == null) + { + return null; + } + _publicKey = await _cryptoFunctionService.RsaExtractPublicKeyAsync(privateKey); + return _publicKey; + } + + public async Task GetPrivateKeyAsync() + { + if (_privateKey != null) + { + return _privateKey; + } + var encPrivateKey = await _stateService.GetPrivateKeyEncryptedAsync(); + if (encPrivateKey == null) + { + return null; + } + _privateKey = await DecryptToBytesAsync(new EncString(encPrivateKey), null); + return _privateKey; + } + + public async Task> GetFingerprintAsync(string userId, byte[] publicKey = null) + { + if (publicKey == null) + { + publicKey = await GetPublicKeyAsync(); + } + if (publicKey == null) + { + throw new Exception("No public key available."); + } + var keyFingerprint = await _cryptoFunctionService.HashAsync(publicKey, CryptoHashAlgorithm.Sha256); + var userFingerprint = await _cryptoFunctionService.HkdfExpandAsync(keyFingerprint, Encoding.UTF8.GetBytes(userId), 32, HkdfAlgorithm.Sha256); + return HashPhrase(userFingerprint); + } + + public Task> GetOrgKeysAsync() + { + if (_orgKeys != null && _orgKeys.Count > 0) + { + return Task.FromResult(_orgKeys); + } + if (_getOrgKeysTask != null && !_getOrgKeysTask.IsCompleted && !_getOrgKeysTask.IsFaulted) + { + return _getOrgKeysTask; + } + async Task> doTask() + { + try + { + var encOrgKeys = await _stateService.GetOrgKeysEncryptedAsync(); + if (encOrgKeys == null) + { + return null; + } + var orgKeys = new Dictionary(); + var setKey = false; + foreach (var org in encOrgKeys) + { + var decValue = await RsaDecryptAsync(org.Value); + orgKeys.Add(org.Key, new SymmetricCryptoKey(decValue)); + setKey = true; + } + + if (setKey) + { + _orgKeys = orgKeys; + } + return _orgKeys; + } + finally + { + _getOrgKeysTask = null; + } + } + _getOrgKeysTask = doTask(); + return _getOrgKeysTask; + } + + public async Task GetOrgKeyAsync(string orgId) + { + if (string.IsNullOrWhiteSpace(orgId)) + { + return null; + } + var orgKeys = await GetOrgKeysAsync(); + if (orgKeys == null || !orgKeys.ContainsKey(orgId)) + { + return null; + } + return orgKeys[orgId]; + } + + public async Task CompareAndUpdateKeyHashAsync(string masterPassword, SymmetricCryptoKey key) + { + var storedKeyHash = await GetKeyHashAsync(); + if (masterPassword != null && storedKeyHash != null) + { + var localKeyHash = await HashPasswordAsync(masterPassword, key, HashPurpose.LocalAuthorization); + if (localKeyHash != null && storedKeyHash == localKeyHash) + { + return true; + } + + var serverKeyHash = await HashPasswordAsync(masterPassword, key, HashPurpose.ServerAuthorization); + if (serverKeyHash != null & storedKeyHash == serverKeyHash) + { + await SetKeyHashAsync(localKeyHash); + return true; + } + } + + return false; + } + + public async Task HasKeyAsync(string userId = null) + { + var key = await GetKeyAsync(userId); + return key != null; + } + + public async Task HasEncKeyAsync() + { + var encKey = await _stateService.GetEncKeyEncryptedAsync(); + return encKey != null; + } + + public async Task ClearKeyAsync(string userId = null) + { + await _stateService.SetKeyDecryptedAsync(null, userId); + _legacyEtmKey = null; + await _stateService.SetKeyEncryptedAsync(null, userId); + } + + public async Task ClearKeyHashAsync(string userId = null) + { + _keyHash = null; + await _stateService.SetKeyHashAsync(null, userId); + } + + public async Task ClearEncKeyAsync(bool memoryOnly = false, string userId = null) + { + _encKey = null; + if (!memoryOnly) + { + await _stateService.SetEncKeyEncryptedAsync(null, userId); + } + } + + public async Task ClearKeyPairAsync(bool memoryOnly = false, string userId = null) + { + _publicKey = _privateKey = null; + if (!memoryOnly) + { + await _stateService.SetPrivateKeyEncryptedAsync(null, userId); + } + } + + public async Task ClearOrgKeysAsync(bool memoryOnly = false, string userId = null) + { + _orgKeys = null; + if (!memoryOnly) + { + await _stateService.SetOrgKeysEncryptedAsync(null, userId); + } + } + + public async Task ClearPinProtectedKeyAsync(string userId = null) + { + await _stateService.SetPinProtectedAsync(null, userId); + } + + public void ClearCache() + { + _encKey = null; + _legacyEtmKey = null; + _keyHash = null; + _publicKey = null; + _privateKey = null; + _orgKeys = null; + } + + public async Task ClearKeysAsync(string userId = null) + { + await Task.WhenAll(new Task[] + { + ClearKeyAsync(userId), + ClearKeyHashAsync(userId), + ClearOrgKeysAsync(false, userId), + ClearEncKeyAsync(false, userId), + ClearKeyPairAsync(false, userId), + ClearPinProtectedKeyAsync(userId) + }); + } + + public async Task ToggleKeyAsync() + { + var key = await GetKeyAsync(); + var option = await _stateService.GetVaultTimeoutAsync(); + var biometric = await _stateService.GetBiometricUnlockAsync(); + if (!biometric.GetValueOrDefault() && (option != null || option == 0)) + { + await ClearKeyAsync(); + await _stateService.SetKeyDecryptedAsync(key); + return; + } + await SetKeyAsync(key); + } + + public async Task MakeKeyAsync(string password, string salt, KdfConfig kdfConfig) + { + byte[] key = null; + if (kdfConfig.Type == null || kdfConfig.Type == KdfType.PBKDF2_SHA256) + { + var iterations = kdfConfig.Iterations.GetValueOrDefault(5000); + if (iterations < 5000) + { + throw new Exception("PBKDF2 iteration minimum is 5000."); + } + key = await _cryptoFunctionService.Pbkdf2Async(password, salt, + CryptoHashAlgorithm.Sha256, iterations); + } + else if (kdfConfig.Type == KdfType.Argon2id) + { + var iterations = kdfConfig.Iterations.GetValueOrDefault(Constants.Argon2Iterations); + var memory = kdfConfig.Memory.GetValueOrDefault(Constants.Argon2MemoryInMB) * 1024; + var parallelism = kdfConfig.Parallelism.GetValueOrDefault(Constants.Argon2Parallelism); + + if (kdfConfig.Iterations < 2) + { + throw new Exception("Argon2 iterations minimum is 2"); + } + + if (kdfConfig.Memory < 16) + { + throw new Exception("Argon2 memory minimum is 16 MB"); + } + else if (kdfConfig.Memory > 1024) + { + throw new Exception("Argon2 memory maximum is 1024 MB"); + } + + if (kdfConfig.Parallelism < 1) + { + throw new Exception("Argon2 parallelism minimum is 1"); + } + + var saltHash = await _cryptoFunctionService.HashAsync(salt, CryptoHashAlgorithm.Sha256); + key = await _cryptoFunctionService.Argon2Async(password, saltHash, iterations, memory, parallelism); + } + else + { + throw new Exception("Unknown kdf."); + } + return new SymmetricCryptoKey(key); + } + + public async Task MakeKeyFromPinAsync(string pin, string salt, + KdfConfig config, EncString protectedKeyCs = null) + { + if (protectedKeyCs == null) + { + var pinProtectedKey = await _stateService.GetPinProtectedAsync(); + if (pinProtectedKey == null) + { + throw new Exception("No PIN protected key found."); + } + protectedKeyCs = new EncString(pinProtectedKey); + } + var pinKey = await MakePinKeyAysnc(pin, salt, config); + var decKey = await DecryptToBytesAsync(protectedKeyCs, pinKey); + return new SymmetricCryptoKey(decKey); + } + + public async Task> MakeShareKeyAsync() + { + var shareKey = await _cryptoFunctionService.RandomBytesAsync(64); + var publicKey = await GetPublicKeyAsync(); + var encShareKey = await RsaEncryptAsync(shareKey, publicKey); + return new Tuple(encShareKey, new SymmetricCryptoKey(shareKey)); + } + + public async Task> MakeKeyPairAsync(SymmetricCryptoKey key = null) + { + var keyPair = await _cryptoFunctionService.RsaGenerateKeyPairAsync(2048); + var publicB64 = Convert.ToBase64String(keyPair.Item1); + var privateEnc = await EncryptAsync(keyPair.Item2, key); + return new Tuple(publicB64, privateEnc); + } + + public async Task MakePinKeyAysnc(string pin, string salt, KdfConfig config) + { + var pinKey = await MakeKeyAsync(pin, salt, config); + return await StretchKeyAsync(pinKey); + } + + public async Task MakeSendKeyAsync(byte[] keyMaterial) + { + var sendKey = await _cryptoFunctionService.HkdfAsync(keyMaterial, "bitwarden-send", "send", 64, HkdfAlgorithm.Sha256); + return new SymmetricCryptoKey(sendKey); + } + + public async Task HashPasswordAsync(string password, SymmetricCryptoKey key, HashPurpose hashPurpose = HashPurpose.ServerAuthorization) + { + if (key == null) + { + key = await GetKeyAsync(); + } + if (password == null || key == null) + { + throw new Exception("Invalid parameters."); + } + var iterations = hashPurpose == HashPurpose.LocalAuthorization ? 2 : 1; + var hash = await _cryptoFunctionService.Pbkdf2Async(key.Key, password, CryptoHashAlgorithm.Sha256, iterations); + return Convert.ToBase64String(hash); + } + + public async Task> MakeEncKeyAsync(SymmetricCryptoKey key) + { + var theKey = await GetKeyForEncryptionAsync(key); + var encKey = await _cryptoFunctionService.RandomBytesAsync(64); + return await BuildEncKeyAsync(theKey, encKey); + } + + public async Task> RemakeEncKeyAsync(SymmetricCryptoKey key) + { + var encKey = await GetEncKeyAsync(); + return await BuildEncKeyAsync(key, encKey.Key); + } + + public async Task EncryptAsync(string plainValue, SymmetricCryptoKey key = null) + { + if (plainValue == null) + { + return null; + } + return await EncryptAsync(Encoding.UTF8.GetBytes(plainValue), key); + } + + public async Task EncryptAsync(byte[] plainValue, SymmetricCryptoKey key = null) + { + if (plainValue == null) + { + return null; + } + var encObj = await AesEncryptAsync(plainValue, key); + var iv = Convert.ToBase64String(encObj.Iv); + var data = Convert.ToBase64String(encObj.Data); + var mac = encObj.Mac != null ? Convert.ToBase64String(encObj.Mac) : null; + return new EncString(encObj.Key.EncType, data, iv, mac); + } + + public async Task EncryptToBytesAsync(byte[] plainValue, SymmetricCryptoKey key = null) + { + var encValue = await AesEncryptAsync(plainValue, key); + var macLen = 0; + if (encValue.Mac != null) + { + macLen = encValue.Mac.Length; + } + var encBytes = new byte[1 + encValue.Iv.Length + macLen + encValue.Data.Length]; + Buffer.BlockCopy(new byte[] { (byte)encValue.Key.EncType }, 0, encBytes, 0, 1); + Buffer.BlockCopy(encValue.Iv, 0, encBytes, 1, encValue.Iv.Length); + if (encValue.Mac != null) + { + Buffer.BlockCopy(encValue.Mac, 0, encBytes, 1 + encValue.Iv.Length, encValue.Mac.Length); + } + Buffer.BlockCopy(encValue.Data, 0, encBytes, 1 + encValue.Iv.Length + macLen, encValue.Data.Length); + return new EncByteArray(encBytes); + } + + public async Task RsaEncryptAsync(byte[] data, byte[] publicKey = null) + { + if (publicKey == null) + { + publicKey = await GetPublicKeyAsync(); + } + if (publicKey == null) + { + throw new Exception("Public key unavailable."); + } + var encBytes = await _cryptoFunctionService.RsaEncryptAsync(data, publicKey, CryptoHashAlgorithm.Sha1); + return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Convert.ToBase64String(encBytes)); + } + + public async Task DecryptToBytesAsync(EncString encString, SymmetricCryptoKey key = null) + { + var iv = Convert.FromBase64String(encString.Iv); + var data = Convert.FromBase64String(encString.Data); + var mac = !string.IsNullOrWhiteSpace(encString.Mac) ? Convert.FromBase64String(encString.Mac) : null; + return await AesDecryptToBytesAsync(encString.EncryptionType, data, iv, mac, key); + } + + public async Task DecryptToUtf8Async(EncString encString, SymmetricCryptoKey key = null) + { + return await AesDecryptToUtf8Async(encString.EncryptionType, encString.Data, + encString.Iv, encString.Mac, key); + } + + public async Task DecryptFromBytesAsync(byte[] encBytes, SymmetricCryptoKey key) + { + if (encBytes == null) + { + throw new Exception("no encBytes."); + } + + var encType = (EncryptionType)encBytes[0]; + byte[] ctBytes = null; + byte[] ivBytes = null; + byte[] macBytes = null; + + switch (encType) + { + case EncryptionType.AesCbc128_HmacSha256_B64: + case EncryptionType.AesCbc256_HmacSha256_B64: + if (encBytes.Length < 49) // 1 + 16 + 32 + ctLength + { + return null; + } + ivBytes = new ArraySegment(encBytes, 1, 16).ToArray(); + macBytes = new ArraySegment(encBytes, 17, 32).ToArray(); + ctBytes = new ArraySegment(encBytes, 49, encBytes.Length - 49).ToArray(); + break; + case EncryptionType.AesCbc256_B64: + if (encBytes.Length < 17) // 1 + 16 + ctLength + { + return null; + } + ivBytes = new ArraySegment(encBytes, 1, 16).ToArray(); + ctBytes = new ArraySegment(encBytes, 17, encBytes.Length - 17).ToArray(); + break; + default: + return null; + } + + return await AesDecryptToBytesAsync(encType, ctBytes, ivBytes, macBytes, key); + } + + public async Task RandomNumberAsync(int min, int max) + { + // Make max inclusive + max = max + 1; + + var diff = (long)max - min; + var upperBound = uint.MaxValue / diff * diff; + uint ui; + do + { + ui = await _cryptoFunctionService.RandomNumberAsync(); + } while (ui >= upperBound); + return (int)(min + (ui % diff)); + } + + /// + /// Makes random string with length based on the charset + /// + public async Task RandomStringAsync(int length) + { + var sb = new StringBuilder(); + + for (var i = 0; i < length; i++) + { + var randomCharIndex = await RandomNumberAsync(0, RANDOM_STRING_CHARSET.Length - 1); + sb.Append(RANDOM_STRING_CHARSET[randomCharIndex]); + } + + return sb.ToString(); + } + + // Helpers + + private async Task AesEncryptAsync(byte[] data, SymmetricCryptoKey key) + { + var obj = new EncryptedObject + { + Key = await GetKeyForEncryptionAsync(key), + Iv = await _cryptoFunctionService.RandomBytesAsync(16) + }; + obj.Data = await _cryptoFunctionService.AesEncryptAsync(data, obj.Iv, obj.Key.EncKey); + if (obj.Key.MacKey != null) + { + var macData = new byte[obj.Iv.Length + obj.Data.Length]; + Buffer.BlockCopy(obj.Iv, 0, macData, 0, obj.Iv.Length); + Buffer.BlockCopy(obj.Data, 0, macData, obj.Iv.Length, obj.Data.Length); + obj.Mac = await _cryptoFunctionService.HmacAsync(macData, obj.Key.MacKey, CryptoHashAlgorithm.Sha256); + } + return obj; + } + + private async Task AesDecryptToUtf8Async(EncryptionType encType, string data, string iv, string mac, + SymmetricCryptoKey key) + { + var keyForEnc = await GetKeyForEncryptionAsync(key); + var theKey = ResolveLegacyKey(encType, keyForEnc); + if (theKey.MacKey != null && mac == null) + { + // Mac required. + return null; + } + if (theKey.EncType != encType) + { + // encType unavailable. + return null; + } + + // "Fast params" conversion + var encKey = theKey.EncKey; + var dataBytes = Convert.FromBase64String(data); + var ivBytes = Convert.FromBase64String(iv); + + var macDataBytes = new byte[ivBytes.Length + dataBytes.Length]; + Buffer.BlockCopy(ivBytes, 0, macDataBytes, 0, ivBytes.Length); + Buffer.BlockCopy(dataBytes, 0, macDataBytes, ivBytes.Length, dataBytes.Length); + + byte[] macKey = null; + if (theKey.MacKey != null) + { + macKey = theKey.MacKey; + } + byte[] macBytes = null; + if (mac != null) + { + macBytes = Convert.FromBase64String(mac); + } + + // Compute mac + if (macKey != null && macBytes != null) + { + var computedMac = await _cryptoFunctionService.HmacAsync(macDataBytes, macKey, + CryptoHashAlgorithm.Sha256); + var macsEqual = await _cryptoFunctionService.CompareAsync(macBytes, computedMac); + if (!macsEqual) + { + // Mac failed + return null; + } + } + + var decBytes = await _cryptoFunctionService.AesDecryptAsync(dataBytes, ivBytes, encKey); + return Encoding.UTF8.GetString(decBytes); + } + + private async Task AesDecryptToBytesAsync(EncryptionType encType, byte[] data, byte[] iv, byte[] mac, + SymmetricCryptoKey key) + { + + var keyForEnc = await GetKeyForEncryptionAsync(key); + var theKey = ResolveLegacyKey(encType, keyForEnc); + if (theKey.MacKey != null && mac == null) + { + // Mac required. + return null; + } + if (theKey.EncType != encType) + { + // encType unavailable. + return null; + } + + // Compute mac + if (theKey.MacKey != null && mac != null) + { + var macData = new byte[iv.Length + data.Length]; + Buffer.BlockCopy(iv, 0, macData, 0, iv.Length); + Buffer.BlockCopy(data, 0, macData, iv.Length, data.Length); + + var computedMac = await _cryptoFunctionService.HmacAsync(macData, theKey.MacKey, + CryptoHashAlgorithm.Sha256); + if (computedMac == null) + { + return null; + } + var macsMatch = await _cryptoFunctionService.CompareAsync(mac, computedMac); + if (!macsMatch) + { + // Mac failed + return null; + } + } + + return await _cryptoFunctionService.AesDecryptAsync(data, iv, theKey.EncKey); + } + + public async Task RsaDecryptAsync(string encValue, byte[] privateKey = null) + { + var headerPieces = encValue.Split('.'); + EncryptionType? encType = null; + string[] encPieces = null; + + if (headerPieces.Length == 1) + { + encType = EncryptionType.Rsa2048_OaepSha256_B64; + encPieces = new string[] { headerPieces[0] }; + } + else if (headerPieces.Length == 2 && Enum.TryParse(headerPieces[0], out EncryptionType type)) + { + encType = type; + encPieces = headerPieces[1].Split('|'); + } + + if (!encType.HasValue) + { + throw new Exception("encType unavailable."); + } + if (encPieces == null || encPieces.Length == 0) + { + throw new Exception("encPieces unavailable."); + } + + var data = Convert.FromBase64String(encPieces[0]); + + if (privateKey is null) + { + privateKey = await GetPrivateKeyAsync(); + } + + if (privateKey == null) + { + throw new Exception("No private key."); + } + + var alg = CryptoHashAlgorithm.Sha1; + switch (encType.Value) + { + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: + alg = CryptoHashAlgorithm.Sha256; + break; + case EncryptionType.Rsa2048_OaepSha1_B64: + case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: + break; + default: + throw new Exception("encType unavailable."); + } + + return await _cryptoFunctionService.RsaDecryptAsync(data, privateKey, alg); + } + + private async Task GetKeyForEncryptionAsync(SymmetricCryptoKey key = null) + { + if (key != null) + { + return key; + } + var encKey = await GetEncKeyAsync(); + if (encKey != null) + { + return encKey; + } + return await GetKeyAsync(); + } + + private SymmetricCryptoKey ResolveLegacyKey(EncryptionType encKey, SymmetricCryptoKey key) + { + if (encKey == EncryptionType.AesCbc128_HmacSha256_B64 && key.EncType == EncryptionType.AesCbc256_B64) + { + // Old encrypt-then-mac scheme, make a new key + if (_legacyEtmKey == null) + { + _legacyEtmKey = new SymmetricCryptoKey(key.Key, EncryptionType.AesCbc128_HmacSha256_B64); + } + return _legacyEtmKey; + } + return key; + } + + private async Task StretchKeyAsync(SymmetricCryptoKey key) + { + var newKey = new byte[64]; + var enc = await _cryptoFunctionService.HkdfExpandAsync(key.Key, Encoding.UTF8.GetBytes("enc"), 32, HkdfAlgorithm.Sha256); + Buffer.BlockCopy(enc, 0, newKey, 0, 32); + var mac = await _cryptoFunctionService.HkdfExpandAsync(key.Key, Encoding.UTF8.GetBytes("mac"), 32, HkdfAlgorithm.Sha256); + Buffer.BlockCopy(mac, 0, newKey, 32, 32); + return new SymmetricCryptoKey(newKey); + } + + private List HashPhrase(byte[] hash, int minimumEntropy = 64) + { + var wordLength = EEFLongWordList.Instance.List.Count; + var entropyPerWord = Math.Log(wordLength) / Math.Log(2); + var numWords = (int)Math.Ceiling(minimumEntropy / entropyPerWord); + + var entropyAvailable = hash.Length * 4; + if (numWords * entropyPerWord > entropyAvailable) + { + throw new Exception("Output entropy of hash function is too small"); + } + + var phrase = new List(); + var hashHex = string.Concat("0", BitConverter.ToString(hash).Replace("-", "")); + var hashNumber = BigInteger.Parse(hashHex, System.Globalization.NumberStyles.HexNumber); + while (numWords-- > 0) + { + var remainder = (int)(hashNumber % wordLength); + hashNumber = hashNumber / wordLength; + phrase.Add(EEFLongWordList.Instance.List[remainder]); + } + return phrase; + } + + private async Task> BuildEncKeyAsync(SymmetricCryptoKey key, + byte[] encKey) + { + EncString encKeyEnc = null; + if (key.Key.Length == 32) + { + var newKey = await StretchKeyAsync(key); + encKeyEnc = await EncryptAsync(encKey, newKey); + } + else if (key.Key.Length == 64) + { + encKeyEnc = await EncryptAsync(encKey, key); + } + else + { + throw new Exception("Invalid key size."); + } + return new Tuple(new SymmetricCryptoKey(encKey), encKeyEnc); + } + + private class EncryptedObject + { + public byte[] Iv { get; set; } + public byte[] Data { get; set; } + public byte[] Mac { get; set; } + public SymmetricCryptoKey Key { get; set; } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/EmailForwarders/AnonAddyForwarder.cs b/src/Maui/Bitwarden/Core/Services/EmailForwarders/AnonAddyForwarder.cs new file mode 100644 index 000000000..834dd863d --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/EmailForwarders/AnonAddyForwarder.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Newtonsoft.Json.Linq; + +namespace Bit.Core.Services.EmailForwarders +{ + public class AnonAddyForwarderOptions : ForwarderOptions + { + public string DomainName { get; set; } + } + + public class AnonAddyForwarder : BaseForwarder + { + protected override string RequestUri => "https://app.anonaddy.com/api/v1/aliases"; + + protected override bool CanGenerate(AnonAddyForwarderOptions options) + { + return !string.IsNullOrWhiteSpace(options.ApiKey) && !string.IsNullOrWhiteSpace(options.DomainName); + } + + protected override void ConfigureHeaders(HttpRequestHeaders headers, AnonAddyForwarderOptions options) + { + headers.Add("Authorization", $"Bearer {options.ApiKey}"); + } + + protected override Task GetContentAsync(IApiService apiService, AnonAddyForwarderOptions options) + { + return Task.FromResult(new FormUrlEncodedContent(new Dictionary + { + ["domain"] = options.DomainName + })); + } + + protected override string HandleResponse(JObject result) + { + return result["data"]?["email"]?.ToString(); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/EmailForwarders/BaseForwarder.cs b/src/Maui/Bitwarden/Core/Services/EmailForwarders/BaseForwarder.cs new file mode 100644 index 000000000..b31393746 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/EmailForwarders/BaseForwarder.cs @@ -0,0 +1,63 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Newtonsoft.Json.Linq; + +namespace Bit.Core.Services.EmailForwarders +{ + public abstract class BaseForwarder + where T : ForwarderOptions + { + protected abstract string RequestUri { get; } + + public async Task GenerateAsync(IApiService apiService, T options) + { + if (!CanGenerate(options)) + { + return Constants.DefaultUsernameGenerated; + } + + using (var requestMessage = new HttpRequestMessage()) + { + requestMessage.Version = new Version(1, 0); + requestMessage.Method = HttpMethod.Post; + requestMessage.RequestUri = new Uri(RequestUri); + requestMessage.Headers.Add("Accept", "application/json"); + + ConfigureHeaders(requestMessage.Headers, options); + requestMessage.Content = await GetContentAsync(apiService, options); + + try + { + var response = await apiService.SendAsync(requestMessage); + + var responseJsonString = await response.Content.ReadAsStringAsync(); + + return HandleResponse(JObject.Parse(responseJsonString)); + } + catch (ApiException ex) + { + if (IsRequestSecretInvalid(ex)) + { + throw new ForwardedEmailInvalidSecretException(ex); + } + + throw; + } + } + } + + protected virtual bool CanGenerate(T options) => !string.IsNullOrWhiteSpace(options.ApiKey); + + protected abstract void ConfigureHeaders(HttpRequestHeaders headers, T options); + + protected abstract Task GetContentAsync(IApiService apiService, T options); + + protected abstract string HandleResponse(JObject result); + + protected virtual bool IsRequestSecretInvalid(ApiException ex) => ex.Error?.StatusCode == System.Net.HttpStatusCode.Unauthorized; + } +} diff --git a/src/Maui/Bitwarden/Core/Services/EmailForwarders/DuckDuckGoForwarder.cs b/src/Maui/Bitwarden/Core/Services/EmailForwarders/DuckDuckGoForwarder.cs new file mode 100644 index 000000000..7e9579be5 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/EmailForwarders/DuckDuckGoForwarder.cs @@ -0,0 +1,25 @@ +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Newtonsoft.Json.Linq; + +namespace Bit.Core.Services.EmailForwarders +{ + public class DuckDuckGoForwarder : BaseForwarder + { + protected override string RequestUri => "https://quack.duckduckgo.com/api/email/addresses"; + + protected override void ConfigureHeaders(HttpRequestHeaders headers, ForwarderOptions options) + { + headers.Add("Authorization", $"Bearer {options.ApiKey}"); + } + + protected override Task GetContentAsync(IApiService apiService, ForwarderOptions options) => Task.FromResult(null); + + protected override string HandleResponse(JObject result) + { + return $"{result["address"]?.ToString()}@duck.com"; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/EmailForwarders/FastmailForwarder.cs b/src/Maui/Bitwarden/Core/Services/EmailForwarders/FastmailForwarder.cs new file mode 100644 index 000000000..a47229861 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/EmailForwarders/FastmailForwarder.cs @@ -0,0 +1,98 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Newtonsoft.Json.Linq; + +namespace Bit.Core.Services.EmailForwarders +{ + public class FastmailForwarder : BaseForwarder + { + protected override string RequestUri => "https://api.fastmail.com/jmap/api/"; + + protected override void ConfigureHeaders(HttpRequestHeaders headers, ForwarderOptions options) + { + headers.Add("Authorization", $"Bearer {options.ApiKey}"); + } + + protected override async Task GetContentAsync(IApiService apiService, ForwarderOptions options) + { + string accountId = null; + try + { + accountId = await apiService.GetFastmailAccountIdAsync(options.ApiKey); + } + catch (ApiException ex) + { + if (IsRequestSecretInvalid(ex)) + { + throw new ForwardedEmailInvalidSecretException(ex); + } + + throw; + } + + var requestJObj = new JObject + { + new JProperty("using", + new JArray { "https://www.fastmail.com/dev/maskedemail", "urn:ietf:params:jmap:core" }), + new JProperty("methodCalls", + new JArray + { + new JArray + { + "MaskedEmail/set", + new JObject + { + ["accountId"] = accountId, + ["create"] = new JObject + { + ["new-masked-email"] = new JObject + { + ["state"] = "enabled", + ["description"] = "", + ["url"] = "", + ["emailPrefix"] = "" + } + } + }, + "0" + } + }) + }; + + return new StringContent(requestJObj.ToString(), Encoding.UTF8, "application/json"); + } + + protected override string HandleResponse(JObject result) + { + if (result["methodResponses"] == null || !result["methodResponses"].HasValues || + !result["methodResponses"][0].HasValues) + { + throw new Exception("Fastmail error: could not parse response."); + } + if (result["methodResponses"][0][0].ToString() == "MaskedEmail/set") + { + if (result["methodResponses"][0][1]?["created"]?["new-masked-email"] != null) + { + return result["methodResponses"][0][1]?["created"]?["new-masked-email"]?["email"].ToString(); + } + if (result["methodResponses"][0][1]?["notCreated"]?["new-masked-email"] != null) + { + throw new Exception("Fastmail error: " + + result["methodResponses"][0][1]?["created"]?["new-masked-email"]?["description"].ToString()); + } + } + else if (result["methodResponses"][0][0].ToString() == "error") + { + throw new Exception("Fastmail error: " + result["methodResponses"][0][1]?["description"].ToString()); + } + throw new Exception("Fastmail error: could not parse response."); + } + + protected override bool IsRequestSecretInvalid(ApiException ex) => base.IsRequestSecretInvalid(ex) || ex.Error?.StatusCode == System.Net.HttpStatusCode.Forbidden; + } +} diff --git a/src/Maui/Bitwarden/Core/Services/EmailForwarders/FirefoxRelayForwarder.cs b/src/Maui/Bitwarden/Core/Services/EmailForwarders/FirefoxRelayForwarder.cs new file mode 100644 index 000000000..0094808f6 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/EmailForwarders/FirefoxRelayForwarder.cs @@ -0,0 +1,36 @@ +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Bit.Core.Services.EmailForwarders +{ + public class FirefoxRelayForwarder : BaseForwarder + { + protected override string RequestUri => "https://relay.firefox.com/api/v1/relayaddresses/"; + + protected override void ConfigureHeaders(HttpRequestHeaders headers, ForwarderOptions options) + { + headers.Add("Authorization", $"Token {options.ApiKey}"); + } + + protected override Task GetContentAsync(IApiService apiService, ForwarderOptions options) + { + return Task.FromResult(new StringContent( + JsonConvert.SerializeObject( + new + { + enabled = true, + description = "Generated by Bitwarden." + }), Encoding.UTF8, "application/json")); + } + + protected override string HandleResponse(JObject result) + { + return result["full_address"]?.ToString(); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/EmailForwarders/ForwarderOptions.cs b/src/Maui/Bitwarden/Core/Services/EmailForwarders/ForwarderOptions.cs new file mode 100644 index 000000000..b640ebb39 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/EmailForwarders/ForwarderOptions.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Services.EmailForwarders +{ + public class ForwarderOptions + { + public string ApiKey { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/EmailForwarders/SimpleLoginForwarder.cs b/src/Maui/Bitwarden/Core/Services/EmailForwarders/SimpleLoginForwarder.cs new file mode 100644 index 000000000..384e4adf2 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/EmailForwarders/SimpleLoginForwarder.cs @@ -0,0 +1,25 @@ +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Newtonsoft.Json.Linq; + +namespace Bit.Core.Services.EmailForwarders +{ + public class SimpleLoginForwarder : BaseForwarder + { + protected override string RequestUri => "https://app.simplelogin.io/api/alias/random/new"; + + protected override void ConfigureHeaders(HttpRequestHeaders headers, ForwarderOptions options) + { + headers.Add("Authentication", options.ApiKey); + } + + protected override Task GetContentAsync(IApiService apiService, ForwarderOptions options) => Task.FromResult(null); + + protected override string HandleResponse(JObject result) + { + return result["alias"]?.ToString(); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/EnvironmentService.cs b/src/Maui/Bitwarden/Core/Services/EnvironmentService.cs new file mode 100644 index 000000000..a7a9dd197 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/EnvironmentService.cs @@ -0,0 +1,147 @@ +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; + +namespace Bit.Core.Services +{ + public class EnvironmentService : IEnvironmentService + { + private const string DEFAULT_WEB_VAULT_URL = "https://vault.bitwarden.com"; + private const string DEFAULT_WEB_SEND_URL = "https://send.bitwarden.com/#"; + + private readonly IApiService _apiService; + private readonly IStateService _stateService; + private readonly IConditionedAwaiterManager _conditionedAwaiterManager; + + public EnvironmentService( + IApiService apiService, + IStateService stateService, + IConditionedAwaiterManager conditionedAwaiterManager) + { + _apiService = apiService; + _stateService = stateService; + _conditionedAwaiterManager = conditionedAwaiterManager; + } + + public string BaseUrl { get; set; } + public string WebVaultUrl { get; set; } + public string ApiUrl { get; set; } + public string IdentityUrl { get; set; } + public string IconsUrl { get; set; } + public string NotificationsUrl { get; set; } + public string EventsUrl { get; set; } + + public string GetWebVaultUrl(bool returnNullIfDefault = false) + { + if (!string.IsNullOrWhiteSpace(WebVaultUrl)) + { + return WebVaultUrl; + } + + if (!string.IsNullOrWhiteSpace(BaseUrl)) + { + return BaseUrl; + } + + return returnNullIfDefault ? (string)null : DEFAULT_WEB_VAULT_URL; + } + + public string GetWebSendUrl() + { + return GetWebVaultUrl(true) is string webVaultUrl ? $"{webVaultUrl}/#/send/" : DEFAULT_WEB_SEND_URL; + } + + public async Task SetUrlsFromStorageAsync() + { + try + { + var urls = await _stateService.GetEnvironmentUrlsAsync(); + if (urls == null) + { + urls = await _stateService.GetPreAuthEnvironmentUrlsAsync(); + } + if (urls == null) + { + urls = new EnvironmentUrlData(); + } + var envUrls = new EnvironmentUrls(); + if (!string.IsNullOrWhiteSpace(urls.Base)) + { + BaseUrl = envUrls.Base = urls.Base; + _apiService.SetUrls(envUrls); + + _conditionedAwaiterManager.SetAsCompleted(AwaiterPrecondition.EnvironmentUrlsInited); + return; + } + + BaseUrl = urls.Base; + WebVaultUrl = urls.WebVault; + ApiUrl = envUrls.Api = urls.Api; + IdentityUrl = envUrls.Identity = urls.Identity; + IconsUrl = urls.Icons; + NotificationsUrl = urls.Notifications; + EventsUrl = envUrls.Events = urls.Events; + _apiService.SetUrls(envUrls); + + _conditionedAwaiterManager.SetAsCompleted(AwaiterPrecondition.EnvironmentUrlsInited); + } + catch (System.Exception ex) + { + _conditionedAwaiterManager.SetException(AwaiterPrecondition.EnvironmentUrlsInited, ex); + throw; + } + + } + + public async Task SetUrlsAsync(EnvironmentUrlData urls) + { + urls.Base = FormatUrl(urls.Base); + urls.WebVault = FormatUrl(urls.WebVault); + urls.Api = FormatUrl(urls.Api); + urls.Identity = FormatUrl(urls.Identity); + urls.Icons = FormatUrl(urls.Icons); + urls.Notifications = FormatUrl(urls.Notifications); + urls.Events = FormatUrl(urls.Events); + await _stateService.SetPreAuthEnvironmentUrlsAsync(urls); + BaseUrl = urls.Base; + WebVaultUrl = urls.WebVault; + ApiUrl = urls.Api; + IdentityUrl = urls.Identity; + IconsUrl = urls.Icons; + NotificationsUrl = urls.Notifications; + EventsUrl = urls.Events; + + var envUrls = new EnvironmentUrls(); + if (!string.IsNullOrWhiteSpace(BaseUrl)) + { + envUrls.Base = BaseUrl; + } + else + { + envUrls.Api = ApiUrl; + envUrls.Identity = IdentityUrl; + envUrls.Events = EventsUrl; + } + + _apiService.SetUrls(envUrls); + return urls; + } + + private string FormatUrl(string url) + { + if (string.IsNullOrWhiteSpace(url)) + { + return null; + } + url = Regex.Replace(url, "\\/+$", string.Empty); + if (!url.StartsWith("http://") && !url.StartsWith("https://")) + { + url = string.Concat("https://", url); + } + return url.Trim(); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/EventService.cs b/src/Maui/Bitwarden/Core/Services/EventService.cs new file mode 100644 index 000000000..45f566f84 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/EventService.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Request; + +namespace Bit.Core.Services +{ + public class EventService : IEventService + { + private readonly IApiService _apiService; + private readonly IStateService _stateService; + private readonly IOrganizationService _organizationService; + private readonly ICipherService _cipherService; + + public EventService( + IApiService apiService, + IStateService stateService, + IOrganizationService organizationService, + ICipherService cipherService) + { + _apiService = apiService; + _stateService = stateService; + _organizationService = organizationService; + _cipherService = cipherService; + } + + public async Task CollectAsync(EventType eventType, string cipherId = null, bool uploadImmediately = false) + { + var authed = await _stateService.IsAuthenticatedAsync(); + if (!authed) + { + return; + } + var organizations = await _organizationService.GetAllAsync(); + if (organizations == null) + { + return; + } + var orgIds = new HashSet(organizations.Where(o => o.UseEvents).Select(o => o.Id)); + if (!orgIds.Any()) + { + return; + } + if (cipherId != null) + { + var cipher = await _cipherService.GetAsync(cipherId); + if (cipher?.OrganizationId == null || !orgIds.Contains(cipher.OrganizationId)) + { + return; + } + } + var eventCollection = await _stateService.GetEventCollectionAsync(); + if (eventCollection == null) + { + eventCollection = new List(); + } + eventCollection.Add(new EventData + { + Type = eventType, + CipherId = cipherId, + Date = DateTime.UtcNow + }); + await _stateService.SetEventCollectionAsync(eventCollection); + if (uploadImmediately) + { + await UploadEventsAsync(); + } + } + + public async Task UploadEventsAsync() + { + var authed = await _stateService.IsAuthenticatedAsync(); + if (!authed) + { + return; + } + var eventCollection = await _stateService.GetEventCollectionAsync(); + if (eventCollection == null || !eventCollection.Any()) + { + return; + } + var request = eventCollection.Select(e => new EventRequest + { + Type = e.Type, + CipherId = e.CipherId, + Date = e.Date + }); + try + { + await _apiService.PostEventsCollectAsync(request); + await ClearEventsAsync(); + } + catch (ApiException) { } + } + + public async Task ClearEventsAsync() + { + await _stateService.SetEventCollectionAsync(null); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/ExportService.cs b/src/Maui/Bitwarden/Core/Services/ExportService.cs new file mode 100644 index 000000000..c5fd0c728 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/ExportService.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Export; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using CsvHelper; +using CsvHelper.Configuration.Attributes; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Bit.Core.Services +{ + public class ExportService : IExportService + { + private readonly IFolderService _folderService; + private readonly ICipherService _cipherService; + private readonly ICryptoService _cryptoService; + + public ExportService( + IFolderService folderService, + ICipherService cipherService, + ICryptoService cryptoService) + { + _folderService = folderService; + _cipherService = cipherService; + _cryptoService = cryptoService; + } + + public async Task GetExport(string format = "csv") + { + if (format == "encrypted_json") + { + var folders = (await _folderService.GetAllAsync()).Where(f => f.Id != null).Select(f => new FolderWithId(f)); + var items = (await _cipherService.GetAllAsync()).Where(c => c.OrganizationId == null && c.DeletedDate == null) + .Select(c => new CipherWithId(c)); + + return await ExportEncryptedJson(folders, items); + } + else + { + var decryptedFolders = await _folderService.GetAllDecryptedAsync(); + var decryptedCiphers = (await _cipherService.GetAllDecryptedAsync()).Where(c => c.DeletedDate == null); + + return format == "csv" ? ExportCsv(decryptedFolders, decryptedCiphers) : ExportJson(decryptedFolders, decryptedCiphers); + } + } + + public Task GetOrganizationExport(string organizationId, string format = "csv") + { + throw new NotImplementedException(); + } + + public string GetFileName(string prefix = null, string extension = "csv") + { + var dateString = DateTime.Now.ToString("yyyyMMddHHmmss"); + + return string.Format("bitwarden{0}_export_{1}.{2}", + !string.IsNullOrEmpty(prefix) ? ("_" + prefix) : string.Empty, dateString, extension); + } + + private void BuildCommonCipher(ExportCipher cipher, CipherView c) + { + cipher.Type = null; + cipher.Name = c.Name; + cipher.Notes = c.Notes; + cipher.Fields = null; + // Login props + cipher.LoginUris = null; + cipher.LoginUsername = null; + cipher.LoginPassword = null; + cipher.LoginTotp = null; + + if (c.Fields != null) + { + foreach (var f in c.Fields) + { + if (cipher.Fields == null) + { + cipher.Fields = ""; + } + else + { + cipher.Fields += "\n"; + } + + cipher.Fields += (f.Name ?? "") + ": " + f.Value; + } + } + + switch (c.Type) + { + case CipherType.Login: + cipher.Type = "login"; + cipher.LoginUsername = c.Login.Username; + cipher.LoginPassword = c.Login.Password; + cipher.LoginTotp = c.Login.Totp; + + if (c.Login.Uris != null) + { + foreach (var u in c.Login.Uris) + { + if (cipher.LoginUris == null) + { + cipher.LoginUris = ""; + } + else + { + cipher.LoginUris += ","; + } + + cipher.LoginUris += u.Uri; + } + } + + break; + case CipherType.SecureNote: + cipher.Type = "note"; + break; + default: + return; + } + } + + private string ExportCsv(IEnumerable decryptedFolders, IEnumerable decryptedCiphers) + { + var foldersMap = decryptedFolders.Where(f => f.Id != null).ToDictionary(f => f.Id); + + var exportCiphers = new List(); + foreach (var c in decryptedCiphers) + { + // only export logins and secure notes + if (c.Type != CipherType.Login && c.Type != CipherType.SecureNote) + { + continue; + } + + if (c.OrganizationId != null) + { + continue; + } + + var cipher = new ExportCipher(); + cipher.Folder = c.FolderId != null && foldersMap.ContainsKey(c.FolderId) + ? foldersMap[c.FolderId].Name : null; + cipher.Favorite = c.Favorite ? "1" : null; + BuildCommonCipher(cipher, c); + exportCiphers.Add(cipher); + } + + using (var writer = new StringWriter()) + using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) + { + csv.WriteRecords(exportCiphers); + csv.Flush(); + return writer.ToString(); + } + } + + private string ExportJson(IEnumerable decryptedFolders, IEnumerable decryptedCiphers) + { + var jsonDoc = new + { + Encrypted = false, + Folders = decryptedFolders.Where(f => f.Id != null).Select(f => new FolderWithId(f)), + Items = decryptedCiphers.Where(c => c.OrganizationId == null) + .Select(c => new CipherWithId(c) { CollectionIds = null }) + }; + + return CoreHelpers.SerializeJson(jsonDoc, + new JsonSerializerSettings + { + Formatting = Formatting.Indented, + ContractResolver = new CamelCasePropertyNamesContractResolver() + }); + } + + private async Task ExportEncryptedJson(IEnumerable folders, IEnumerable ciphers) + { + var encKeyValidation = await _cryptoService.EncryptAsync(Guid.NewGuid().ToString()); + + var jsonDoc = new + { + Encrypted = true, + EncKeyValidation_DO_NOT_EDIT = encKeyValidation.EncryptedString, + Folders = folders, + Items = ciphers, + }; + + return CoreHelpers.SerializeJson(jsonDoc, + new JsonSerializerSettings + { + Formatting = Formatting.Indented, + ContractResolver = new CamelCasePropertyNamesContractResolver() + }); + } + + private class ExportCipher + { + [Name("folder")] + public string Folder { get; set; } + [Name("favorite")] + public string Favorite { get; set; } + [Name("type")] + public string Type { get; set; } + [Name("name")] + public string Name { get; set; } + [Name("notes")] + public string Notes { get; set; } + [Name("fields")] + public string Fields { get; set; } + [Name("login_uri")] + public string LoginUris { get; set; } + [Name("login_username")] + public string LoginUsername { get; set; } + [Name("login_password")] + public string LoginPassword { get; set; } + [Name("login_totp")] + public string LoginTotp { get; set; } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/FileUploadService.cs b/src/Maui/Bitwarden/Core/Services/FileUploadService.cs new file mode 100644 index 000000000..fbc04acd7 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/FileUploadService.cs @@ -0,0 +1,83 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Response; + +namespace Bit.Core.Services +{ + public class FileUploadService : IFileUploadService + { + public FileUploadService(ApiService apiService) + { + _apiService = apiService; + _bitwardenFileUploadService = new BitwardenFileUploadService(apiService); + _azureFileUploadService = new AzureFileUploadService(); + } + + private readonly BitwardenFileUploadService _bitwardenFileUploadService; + private readonly AzureFileUploadService _azureFileUploadService; + private readonly ApiService _apiService; + + public async Task UploadCipherAttachmentFileAsync(AttachmentUploadDataResponse uploadData, + EncString encryptedFileName, EncByteArray encryptedFileData) + { + try + { + switch (uploadData.FileUploadType) + { + case FileUploadType.Direct: + await _bitwardenFileUploadService.Upload(encryptedFileName.EncryptedString, encryptedFileData, + fd => _apiService.PostAttachmentFileAsync(uploadData.CipherResponse.Id, uploadData.AttachmentId, fd)); + break; + case FileUploadType.Azure: + Func> renewalCallback = async () => + { + var response = await _apiService.RenewAttachmentUploadUrlAsync(uploadData.CipherResponse.Id, uploadData.AttachmentId); + return response.Url; + }; + await _azureFileUploadService.Upload(uploadData.Url, encryptedFileData, renewalCallback); + break; + default: + throw new Exception($"Unkown file upload type: {uploadData.FileUploadType}"); + } + } + catch + { + await _apiService.DeleteCipherAttachmentAsync(uploadData.CipherResponse.Id, uploadData.AttachmentId); + throw; + } + } + + public async Task UploadSendFileAsync(SendFileUploadDataResponse uploadData, EncString fileName, EncByteArray encryptedFileData) + { + try + { + switch (uploadData.FileUploadType) + { + case FileUploadType.Direct: + await _bitwardenFileUploadService.Upload(fileName.EncryptedString, encryptedFileData, + fd => _apiService.PostSendFileAsync(uploadData.SendResponse.Id, uploadData.SendResponse.File.Id, fd)); + break; + case FileUploadType.Azure: + Func> renewalCallback = async () => + { + var response = await _apiService.RenewFileUploadUrlAsync(uploadData.SendResponse.Id, uploadData.SendResponse.File.Id); + return response.Url; + }; + + await _azureFileUploadService.Upload(uploadData.Url, encryptedFileData, renewalCallback); + break; + default: + throw new Exception("Unknown file upload type"); + } + } + catch (Exception) + { + await _apiService.DeleteSendAsync(uploadData.SendResponse.Id); + throw; + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/FolderService.cs b/src/Maui/Bitwarden/Core/Services/FolderService.cs new file mode 100644 index 000000000..044ee2b93 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/FolderService.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; +using Bit.Core.Models.Response; +using Bit.Core.Models.View; +using Bit.Core.Utilities; + +namespace Bit.Core.Services +{ + public class FolderService : IFolderService + { + private const char NestingDelimiter = '/'; + + private List _decryptedFolderCache; + private readonly ICryptoService _cryptoService; + private readonly IStateService _stateService; + private readonly IApiService _apiService; + private readonly II18nService _i18nService; + private readonly ICipherService _cipherService; + + public FolderService( + ICryptoService cryptoService, + IStateService stateService, + IApiService apiService, + II18nService i18nService, + ICipherService cipherService) + { + _cryptoService = cryptoService; + _stateService = stateService; + _apiService = apiService; + _i18nService = i18nService; + _cipherService = cipherService; + } + + public void ClearCache() + { + _decryptedFolderCache = null; + } + + public async Task EncryptAsync(FolderView model, SymmetricCryptoKey key = null) + { + var folder = new Folder + { + Id = model.Id, + Name = await _cryptoService.EncryptAsync(model.Name, key) + }; + return folder; + } + + public async Task GetAsync(string id) + { + var folders = await _stateService.GetEncryptedFoldersAsync(); + if (!folders?.ContainsKey(id) ?? true) + { + return null; + } + return new Folder(folders[id]); + } + + public async Task> GetAllAsync() + { + var folders = await _stateService.GetEncryptedFoldersAsync(); + var response = folders?.Select(f => new Folder(f.Value)); + return response?.ToList() ?? new List(); + } + + // TODO: sequentialize? + public async Task> GetAllDecryptedAsync() + { + if (_decryptedFolderCache != null) + { + return _decryptedFolderCache; + } + var hasKey = await _cryptoService.HasKeyAsync(); + if (!hasKey) + { + throw new Exception("No key."); + } + var decFolders = new List(); + async Task decryptAndAddFolderAsync(Folder folder) + { + var f = await folder.DecryptAsync(); + decFolders.Add(f); + } + var tasks = new List(); + var folders = await GetAllAsync(); + foreach (var folder in folders) + { + tasks.Add(decryptAndAddFolderAsync(folder)); + } + await Task.WhenAll(tasks); + decFolders = decFolders.OrderBy(f => f, new FolderLocaleComparer(_i18nService)).ToList(); + + var noneFolder = new FolderView + { + Name = _i18nService.T("FolderNone") + }; + decFolders.Add(noneFolder); + + _decryptedFolderCache = decFolders; + return _decryptedFolderCache; + } + + public async Task>> GetAllNestedAsync(List folders = null) + { + if (folders == null) + { + folders = await GetAllDecryptedAsync(); + } + var nodes = new List>(); + foreach (var f in folders) + { + var folderCopy = new FolderView + { + Id = f.Id, + RevisionDate = f.RevisionDate + }; + var parts = f.Name != null ? + Regex.Replace(f.Name, "^\\/+|\\/+$", string.Empty).Split(NestingDelimiter) : new string[] { }; + CoreHelpers.NestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter); + } + return nodes; + } + + public async Task> GetNestedAsync(string id) + { + var folders = await GetAllNestedAsync(); + return CoreHelpers.GetTreeNodeObject(folders, id); + } + + public async Task SaveWithServerAsync(Folder folder) + { + var request = new FolderRequest(folder); + FolderResponse response; + if (folder.Id == null) + { + response = await _apiService.PostFolderAsync(request); + folder.Id = response.Id; + } + else + { + response = await _apiService.PutFolderAsync(folder.Id, request); + } + var userId = await _stateService.GetActiveUserIdAsync(); + var data = new FolderData(response, userId); + await UpsertAsync(data); + } + + public async Task UpsertAsync(FolderData folder) + { + var folders = await _stateService.GetEncryptedFoldersAsync(); + if (folders == null) + { + folders = new Dictionary(); + } + if (!folders.ContainsKey(folder.Id)) + { + folders.Add(folder.Id, null); + } + folders[folder.Id] = folder; + await _stateService.SetEncryptedFoldersAsync(folders); + _decryptedFolderCache = null; + } + + public async Task UpsertAsync(List folder) + { + var folders = await _stateService.GetEncryptedFoldersAsync(); + if (folders == null) + { + folders = new Dictionary(); + } + foreach (var f in folder) + { + if (!folders.ContainsKey(f.Id)) + { + folders.Add(f.Id, null); + } + folders[f.Id] = f; + } + await _stateService.SetEncryptedFoldersAsync(folders); + _decryptedFolderCache = null; + } + + public async Task ReplaceAsync(Dictionary folders) + { + await _stateService.SetEncryptedFoldersAsync(folders); + _decryptedFolderCache = null; + } + + public async Task ClearAsync(string userId) + { + await _stateService.SetEncryptedFoldersAsync(null, userId); + _decryptedFolderCache = null; + } + + public async Task DeleteAsync(string id) + { + var folders = await _stateService.GetEncryptedFoldersAsync(); + if (folders == null || !folders.ContainsKey(id)) + { + return; + } + folders.Remove(id); + await _stateService.SetEncryptedFoldersAsync(folders); + _decryptedFolderCache = null; + + // Items in a deleted folder are re-assigned to "No Folder" + var ciphers = await _stateService.GetEncryptedCiphersAsync(); + if (ciphers != null) + { + var updates = new List(); + foreach (var c in ciphers) + { + if (c.Value.FolderId == id) + { + c.Value.FolderId = null; + updates.Add(c.Value); + } + } + if (updates.Any()) + { + await _cipherService.UpsertAsync(updates); + } + } + } + + public async Task DeleteWithServerAsync(string id) + { + await _apiService.DeleteFolderAsync(id); + await DeleteAsync(id); + } + + private class FolderLocaleComparer : IComparer + { + private readonly II18nService _i18nService; + + public FolderLocaleComparer(II18nService i18nService) + { + _i18nService = i18nService; + } + + public int Compare(FolderView a, FolderView b) + { + var aName = a?.Name; + var bName = b?.Name; + if (aName == null && bName != null) + { + return -1; + } + if (aName != null && bName == null) + { + return 1; + } + if (aName == null && bName == null) + { + return 0; + } + return _i18nService.StringComparer.Compare(aName, bName); + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/InMemoryStorageService.cs b/src/Maui/Bitwarden/Core/Services/InMemoryStorageService.cs new file mode 100644 index 000000000..164565dab --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/InMemoryStorageService.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Newtonsoft.Json; + +namespace Bit.Core.Services +{ + public class InMemoryStorageService : IStorageService + { + private readonly Dictionary _dict = new Dictionary(); + + public Task GetAsync(string key) + { + if (!_dict.ContainsKey(key)) + { + return Task.FromResult(default(T)); + } + return Task.FromResult(JsonConvert.DeserializeObject(_dict[key])); + } + + public Task SaveAsync(string key, T obj) + { + if (obj == null) + { + return RemoveAsync(key); + } + _dict.Add(key, JsonConvert.SerializeObject(obj)); + return Task.FromResult(0); + } + + public Task RemoveAsync(string key) + { + if (_dict.ContainsKey(key)) + { + _dict.Remove(key); + } + return Task.FromResult(0); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/KeyConnectorService.cs b/src/Maui/Bitwarden/Core/Services/KeyConnectorService.cs new file mode 100644 index 000000000..dbeb8cd06 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/KeyConnectorService.cs @@ -0,0 +1,87 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; + +namespace Bit.Core.Services +{ + public class KeyConnectorService : IKeyConnectorService + { + private readonly IStateService _stateService; + private readonly ICryptoService _cryptoService; + private readonly ITokenService _tokenService; + private readonly IApiService _apiService; + private readonly IOrganizationService _organizationService; + + public KeyConnectorService(IStateService stateService, ICryptoService cryptoService, + ITokenService tokenService, IApiService apiService, OrganizationService organizationService) + { + _stateService = stateService; + _cryptoService = cryptoService; + _tokenService = tokenService; + _apiService = apiService; + _organizationService = organizationService; + } + + public async Task GetAndSetKey(string url) + { + try + { + var userKeyResponse = await _apiService.GetUserKeyFromKeyConnector(url); + var keyArr = Convert.FromBase64String(userKeyResponse.Key); + var k = new SymmetricCryptoKey(keyArr); + await _cryptoService.SetKeyAsync(k); + } + catch (Exception e) + { + throw new Exception("Unable to reach Key Connector", e); + } + } + + public async Task SetUsesKeyConnector(bool usesKeyConnector) + { + await _stateService.SetUsesKeyConnectorAsync(usesKeyConnector); + } + + public async Task GetUsesKeyConnector() + { + return await _stateService.GetUsesKeyConnectorAsync(); + } + + public async Task GetManagingOrganization() + { + var orgs = await _organizationService.GetAllAsync(); + return orgs.Find(o => + o.UsesKeyConnector && + !o.IsAdmin); + } + + public async Task MigrateUser() + { + var organization = await GetManagingOrganization(); + var key = await _cryptoService.GetKeyAsync(); + + try + { + var keyConnectorRequest = new KeyConnectorUserKeyRequest(key.EncKeyB64); + await _apiService.PostUserKeyToKeyConnector(organization.KeyConnectorUrl, keyConnectorRequest); + } + catch (Exception e) + { + throw new Exception("Unable to reach Key Connector", e); + } + + await _apiService.PostConvertToKeyConnector(); + } + + public async Task UserNeedsMigration() + { + var loggedInUsingSso = await _tokenService.GetIsExternal(); + var requiredByOrganization = await GetManagingOrganization() != null; + var userIsNotUsingKeyConnector = !await GetUsesKeyConnector(); + + return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/LiteDbStorageService.cs b/src/Maui/Bitwarden/Core/Services/LiteDbStorageService.cs new file mode 100644 index 000000000..48f1325ba --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/LiteDbStorageService.cs @@ -0,0 +1,123 @@ +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using LiteDB; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Bit.Core.Services +{ + public class LiteDbStorageService : IStorageService + { + private static readonly object _lock = new object(); + + private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + private readonly string _dbPath; + + public LiteDbStorageService(string dbPath) + { + _dbPath = dbPath; + } + + private LiteDatabase GetDb() + { + return new LiteDatabase($"Filename={_dbPath};Upgrade=true;"); + } + + private ILiteCollection GetCollection(LiteDatabase db) + { + return db?.GetCollection("json_items"); + } + + public Task GetAsync(string key) + { + lock (_lock) + { + LiteDatabase db = null; + try + { + db = GetDb(); + var collection = GetCollection(db); + if (db == null || collection == null) + { + return Task.FromResult(default(T)); + } + var item = collection.Find(i => i.Id == key).FirstOrDefault(); + if (item == null) + { + return Task.FromResult(default(T)); + } + return Task.FromResult(JsonConvert.DeserializeObject(item.Value, _jsonSettings)); + } + finally + { + db?.Dispose(); + } + } + } + + public Task SaveAsync(string key, T obj) + { + lock (_lock) + { + LiteDatabase db = null; + try + { + db = GetDb(); + var collection = GetCollection(db); + if (db == null || collection == null) + { + return Task.CompletedTask; + } + var data = JsonConvert.SerializeObject(obj, _jsonSettings); + collection.Upsert(new JsonItem(key, data)); + return Task.CompletedTask; + } + finally + { + db?.Dispose(); + } + } + } + + public Task RemoveAsync(string key) + { + lock (_lock) + { + LiteDatabase db = null; + try + { + db = GetDb(); + var collection = GetCollection(db); + if (db == null || collection == null) + { + return Task.CompletedTask; + } + collection.DeleteMany(i => i.Id == key); + return Task.CompletedTask; + } + finally + { + db?.Dispose(); + } + } + } + + private class JsonItem + { + public JsonItem() { } + + public JsonItem(string key, string value) + { + Id = key; + Value = value; + } + + public string Id { get; set; } + public string Value { get; set; } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/Logging/DebugLogger.cs b/src/Maui/Bitwarden/Core/Services/Logging/DebugLogger.cs new file mode 100644 index 000000000..a52c1de5d --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/Logging/DebugLogger.cs @@ -0,0 +1,57 @@ +#if !FDROID +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Bit.Core.Abstractions; + +namespace Bit.Core.Services +{ + public class DebugLogger : ILogger + { + static ILogger _instance; + public static ILogger Instance + { + get + { + if (_instance is null) + { + _instance = new DebugLogger(); + } + return _instance; + } + } + + protected DebugLogger() + { + } + + public void Error(string message, IDictionary extraData = null, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) + { + var classAndMethod = $"{Path.GetFileNameWithoutExtension(sourceFilePath)}.{memberName}"; + var filePathAndLineNumber = $"{Path.GetFileName(sourceFilePath)}:{sourceLineNumber}"; + + if (string.IsNullOrEmpty(message)) + { + Debug.WriteLine($"Error found in: {classAndMethod})"); + return; + } + + Debug.WriteLine($"File: {filePathAndLineNumber}"); + Debug.WriteLine($"Method: {memberName}"); + Debug.WriteLine($"Message: {message}"); + + } + + public void Exception(Exception ex) => Debug.WriteLine(ex); + + public Task InitAsync() => Task.CompletedTask; + + public Task IsEnabled() => Task.FromResult(true); + + public Task SetEnabled(bool value) => Task.CompletedTask; + } +} +#endif diff --git a/src/Maui/Bitwarden/Core/Services/Logging/Logger.cs b/src/Maui/Bitwarden/Core/Services/Logging/Logger.cs new file mode 100644 index 000000000..d22af6df5 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/Logging/Logger.cs @@ -0,0 +1,136 @@ +#if !FDROID +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using Microsoft.AppCenter; +using Microsoft.AppCenter.Crashes; +using Newtonsoft.Json; + +namespace Bit.Core.Services +{ + public class Logger : ILogger + { + private const string iOSAppSecret = "51f96ae5-68ba-45f6-99a1-8ad9f63046c3"; + private const string DroidAppSecret = "d3834185-b4a6-4347-9047-b86c65293d42"; + + private string _userId; + private string _appId; + private bool _isInitialised = false; + + static ILogger _instance; + public static ILogger Instance + { + get + { + if (_instance is null) + { + _instance = new Logger(); + } + return _instance; + } + } + + protected Logger() + { + } + + + public string Description + { + get + { + return JsonConvert.SerializeObject(new + { + AppId = _appId, + UserId = _userId + }, Formatting.Indented); + } + } + + public async Task InitAsync() + { + if (_isInitialised) + { + return; + } + + var device = ServiceContainer.Resolve("platformUtilsService").GetDevice(); + _userId = await ServiceContainer.Resolve("stateService").GetActiveUserIdAsync(); + _appId = await ServiceContainer.Resolve("appIdService").GetAppIdAsync(); + + switch (device) + { + case Enums.DeviceType.Android: + AppCenter.Start(DroidAppSecret, typeof(Crashes)); + break; + case Enums.DeviceType.iOS: + AppCenter.Start(iOSAppSecret, typeof(Crashes)); + break; + default: + throw new AppCenterException("Cannot start AppCenter. Device type is not configured."); + + } + + AppCenter.SetUserId(_userId); + + Crashes.GetErrorAttachments = (ErrorReport report) => + { + return new ErrorAttachmentLog[] + { + ErrorAttachmentLog.AttachmentWithText(Description, "crshdesc.txt"), + }; + }; + + _isInitialised = true; + } + + public async Task IsEnabled() => await AppCenter.IsEnabledAsync(); + + public async Task SetEnabled(bool value) => await AppCenter.SetEnabledAsync(value); + + public void Error(string message, + IDictionary extraData = null, + [CallerMemberName] string memberName = "", + [CallerFilePath] string sourceFilePath = "", + [CallerLineNumber] int sourceLineNumber = 0) + { + var classAndMethod = $"{Path.GetFileNameWithoutExtension(sourceFilePath)}.{memberName}"; + var filePathAndLineNumber = $"{Path.GetFileName(sourceFilePath)}:{sourceLineNumber}"; + var properties = new Dictionary + { + ["File"] = filePathAndLineNumber, + ["Method"] = memberName + }; + + var exception = new Exception(message ?? $"Error found in: {classAndMethod}"); + if (extraData == null) + { + Crashes.TrackError(exception, properties); + } + else + { + var data = properties.Concat(extraData).ToDictionary(x => x.Key, x => x.Value); + Crashes.TrackError(exception, data); + } + } + + public void Exception(Exception exception) + { + try + { + Crashes.TrackError(exception); + } + catch (Exception ex) + { + Debug.WriteLine(ex.Message); + } + } + } +} +#endif diff --git a/src/Maui/Bitwarden/Core/Services/Logging/LoggerHelper.cs b/src/Maui/Bitwarden/Core/Services/Logging/LoggerHelper.cs new file mode 100644 index 000000000..9cdf225f6 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/Logging/LoggerHelper.cs @@ -0,0 +1,31 @@ +using System; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; + +namespace Bit.Core.Services +{ + public static class LoggerHelper + { + /// + /// Logs the exception even if the service can't be resolved. + /// Useful when we need to log an exception in situations where the ServiceContainer may not be initialized. + /// + /// + public static void LogEvenIfCantBeResolved(Exception ex) + { + if (ServiceContainer.Resolve("logger", true) is ILogger logger) + { + logger.Exception(ex); + } + else + { +#if !FDROID + // just in case the caller throws the exception in a moment where the logger can't be resolved + // we need to track the error as well + Microsoft.AppCenter.Crashes.Crashes.TrackError(ex); +#endif + + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/Logging/StubLogger.cs b/src/Maui/Bitwarden/Core/Services/Logging/StubLogger.cs new file mode 100644 index 000000000..2bb8bcd45 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/Logging/StubLogger.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Bit.Core.Abstractions; + +namespace Bit.Core.Services +{ + /// + /// A logger that does nothing, this is useful on e.g. FDroid, where we cannot use logging through AppCenter + /// + public class StubLogger : ILogger + { + public void Error(string message, IDictionary extraData = null, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) + { + } + + public void Exception(Exception ex) + { + } + + public Task InitAsync() => Task.CompletedTask; + + public Task IsEnabled() => Task.FromResult(false); + + public Task SetEnabled(bool value) => Task.CompletedTask; + } +} diff --git a/src/Maui/Bitwarden/Core/Services/OrganizationService.cs b/src/Maui/Bitwarden/Core/Services/OrganizationService.cs new file mode 100644 index 000000000..566096c1e --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/OrganizationService.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; +using Bit.Core.Models.Response; +using Bit.Core.Utilities; + +namespace Bit.Core.Services +{ + public class OrganizationService : IOrganizationService + { + private readonly IStateService _stateService; + private readonly IApiService _apiService; + + public OrganizationService(IStateService stateService, IApiService apiService) + { + _stateService = stateService; + _apiService = apiService; + } + + public async Task GetAsync(string id) + { + var organizations = await _stateService.GetOrganizationsAsync(); + if (organizations == null || !organizations.ContainsKey(id)) + { + return null; + } + return new Organization(organizations[id]); + } + + public async Task GetByIdentifierAsync(string identifier) + { + var organizations = await GetAllAsync(); + if (organizations == null || organizations.Count == 0) + { + return null; + } + return organizations.FirstOrDefault(o => o.Identifier == identifier); + } + + public async Task> GetAllAsync(string userId = null) + { + var organizations = await _stateService.GetOrganizationsAsync(userId); + return organizations?.Select(o => new Organization(o.Value)).ToList() ?? new List(); + } + + public async Task ReplaceAsync(Dictionary organizations) + { + await _stateService.SetOrganizationsAsync(organizations); + } + + public async Task ClearAllAsync(string userId) + { + await _stateService.SetOrganizationsAsync(null, userId); + } + + public async Task GetClaimedOrganizationDomainAsync(string userEmail) + { + try + { + if (string.IsNullOrEmpty(userEmail)) + { + return null; + } + + return await _apiService.GetOrgDomainSsoDetailsAsync(userEmail); + } + catch (ApiException ex) when (ex.Error?.StatusCode == HttpStatusCode.NotFound) + { + // this is a valid case so there is no need to show an error + return null; + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/PasswordGenerationService.cs b/src/Maui/Bitwarden/Core/Services/PasswordGenerationService.cs new file mode 100644 index 000000000..3210cccad --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/PasswordGenerationService.cs @@ -0,0 +1,547 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; +using Zxcvbn; + +namespace Bit.Core.Services +{ + public class PasswordGenerationService : IPasswordGenerationService + { + private const int MAX_PASSWORDS_IN_HISTORY = 100; + private const string LOWERCASE_CHAR_SET = "abcdefghijkmnopqrstuvwxyz"; + private const string UPPERCASE_CHAR_SET = "ABCDEFGHJKLMNPQRSTUVWXYZ"; + private const string NUMER_CHAR_SET = "23456789"; + private const string SPECIAL_CHAR_SET = "!@#$%^&*"; + + private readonly ICryptoService _cryptoService; + private readonly IStateService _stateService; + private readonly ICryptoFunctionService _cryptoFunctionService; + private readonly IPolicyService _policyService; + private PasswordGenerationOptions _defaultOptions = PasswordGenerationOptions.CreateDefault; + private PasswordGenerationOptions _optionsCache; + private List _history; + + public PasswordGenerationService( + ICryptoService cryptoService, + IStateService stateService, + ICryptoFunctionService cryptoFunctionService, + IPolicyService policyService) + { + _cryptoService = cryptoService; + _stateService = stateService; + _cryptoFunctionService = cryptoFunctionService; + _policyService = policyService; + } + + public async Task GeneratePasswordAsync(PasswordGenerationOptions options) + { + // Overload defaults with given options + options.Merge(_defaultOptions); + if (options.Type == PasswordGenerationOptions.TYPE_PASSPHRASE) + { + return await GeneratePassphraseAsync(options); + } + + // Sanitize + SanitizePasswordLength(options, true); + + var positionsBuilder = new StringBuilder(); + if (options.Lowercase.GetValueOrDefault() && options.MinLowercase > 0) + { + for (int i = 0; i < options.MinLowercase; i++) + { + positionsBuilder.Append("l"); + } + } + if (options.Uppercase.GetValueOrDefault() && options.MinUppercase > 0) + { + for (int i = 0; i < options.MinUppercase; i++) + { + positionsBuilder.Append("u"); + } + } + if (options.Number.GetValueOrDefault() && options.MinNumber > 0) + { + for (int i = 0; i < options.MinNumber; i++) + { + positionsBuilder.Append("n"); + } + } + if (options.Special.GetValueOrDefault() && options.MinSpecial > 0) + { + for (int i = 0; i < options.MinSpecial; i++) + { + positionsBuilder.Append("s"); + } + } + while (positionsBuilder.Length < options.Length.GetValueOrDefault()) + { + positionsBuilder.Append("a"); + } + + // Shuffle + var positions = positionsBuilder.ToString().ToCharArray() + .OrderBy(a => _cryptoFunctionService.RandomNumber()).ToArray(); + + // Build out other character sets + var allCharSet = new StringBuilder(); + + var lowercaseCharSet = LOWERCASE_CHAR_SET; + if (options.AllowAmbiguousChar.GetValueOrDefault()) + { + lowercaseCharSet = string.Concat(lowercaseCharSet, "l"); + } + if (options.Lowercase.GetValueOrDefault()) + { + allCharSet.Append(lowercaseCharSet); + } + + var uppercaseCharSet = UPPERCASE_CHAR_SET; + if (options.AllowAmbiguousChar.GetValueOrDefault()) + { + uppercaseCharSet = string.Concat(uppercaseCharSet, "IO"); + } + if (options.Uppercase.GetValueOrDefault()) + { + allCharSet.Append(uppercaseCharSet); + } + + var numberCharSet = NUMER_CHAR_SET; + if (options.AllowAmbiguousChar.GetValueOrDefault()) + { + numberCharSet = string.Concat(numberCharSet, "01"); + } + if (options.Number.GetValueOrDefault()) + { + allCharSet.Append(numberCharSet); + } + + if (options.Special.GetValueOrDefault()) + { + allCharSet.Append(SPECIAL_CHAR_SET); + } + + var password = new StringBuilder(); + for (var i = 0; i < options.Length.GetValueOrDefault(); i++) + { + var charSetOnCurrentPosition = string.Empty; + switch (positions[i]) + { + case 'l': + charSetOnCurrentPosition = lowercaseCharSet; + break; + case 'u': + charSetOnCurrentPosition = uppercaseCharSet; + break; + case 'n': + charSetOnCurrentPosition = numberCharSet; + break; + case 's': + charSetOnCurrentPosition = SPECIAL_CHAR_SET; + break; + case 'a': + charSetOnCurrentPosition = allCharSet.ToString(); + break; + } + + var randomCharIndex = await _cryptoService.RandomNumberAsync(0, charSetOnCurrentPosition.Length - 1); + password.Append(charSetOnCurrentPosition[randomCharIndex]); + } + + return password.ToString(); + } + + public void ClearCache() + { + _optionsCache = null; + _history = null; + } + + public async Task GeneratePassphraseAsync(PasswordGenerationOptions options) + { + options.Merge(_defaultOptions); + if (options.NumWords <= 2) + { + options.NumWords = _defaultOptions.NumWords; + } + if (options.WordSeparator == null || options.WordSeparator.Length == 0 || options.WordSeparator.Length > 1) + { + options.WordSeparator = " "; + } + if (options.Capitalize == null) + { + options.Capitalize = false; + } + if (options.IncludeNumber == null) + { + options.IncludeNumber = false; + } + var listLength = EEFLongWordList.Instance.List.Count - 1; + var wordList = new List(); + for (int i = 0; i < options.NumWords.GetValueOrDefault(); i++) + { + var wordIndex = await _cryptoService.RandomNumberAsync(0, listLength); + if (options.Capitalize.GetValueOrDefault()) + { + wordList.Add(Capitalize(EEFLongWordList.Instance.List[wordIndex])); + } + else + { + wordList.Add(EEFLongWordList.Instance.List[wordIndex]); + } + } + if (options.IncludeNumber.GetValueOrDefault()) + { + await AppendRandomNumberToRandomWordAsync(wordList); + } + return string.Join(options.WordSeparator, wordList); + } + + public async Task<(PasswordGenerationOptions, PasswordGeneratorPolicyOptions)> GetOptionsAsync() + { + if (_optionsCache == null) + { + var options = await _stateService.GetPasswordGenerationOptionsAsync(); + if (options == null) + { + _optionsCache = _defaultOptions; + } + else + { + options.Merge(_defaultOptions); + _optionsCache = options; + } + } + + var policyOptions = await _policyService.GetPasswordGeneratorPolicyOptionsAsync(); + _optionsCache.EnforcePolicy(policyOptions); + + return (_optionsCache, policyOptions ?? new PasswordGeneratorPolicyOptions()); + } + + public List GetPasswordStrengthUserInput(string email) + { + var atPosition = email?.IndexOf('@'); + if (atPosition is null || atPosition < 0) + { + return null; + } + var rx = new Regex("/[^A-Za-z0-9]/", RegexOptions.Compiled); + var data = rx.Split(email.Substring(0, atPosition.Value).Trim().ToLower()); + + return new List(data); + } + + public async Task SaveOptionsAsync(PasswordGenerationOptions options) + { + await _stateService.SetPasswordGenerationOptionsAsync(options); + _optionsCache = options; + } + + public async Task> GetHistoryAsync() + { + var hasKey = await _cryptoService.HasKeyAsync(); + if (!hasKey) + { + return new List(); + } + if (_history == null) + { + var encrypted = await _stateService.GetEncryptedPasswordGenerationHistory(); + _history = await DecryptHistoryAsync(encrypted); + } + return _history ?? new List(); + } + + public async Task AddHistoryAsync(string password, CancellationToken token = default(CancellationToken)) + { + var hasKey = await _cryptoService.HasKeyAsync(); + if (!hasKey) + { + return; + } + var currentHistory = await GetHistoryAsync(); + // Prevent duplicates + if (MatchesPrevious(password, currentHistory)) + { + return; + } + token.ThrowIfCancellationRequested(); + currentHistory.Insert(0, new GeneratedPasswordHistory { Password = password, Date = DateTime.UtcNow }); + // Remove old items. + if (currentHistory.Count > MAX_PASSWORDS_IN_HISTORY) + { + currentHistory.RemoveAt(currentHistory.Count - 1); + } + var newHistory = await EncryptHistoryAsync(currentHistory); + token.ThrowIfCancellationRequested(); + await _stateService.SetEncryptedPasswordGenerationHistoryAsync(newHistory); + } + + public async Task ClearAsync(string userId = null) + { + _history = new List(); + await _stateService.SetEncryptedPasswordGenerationHistoryAsync(null, userId); + } + + public Result PasswordStrength(string password, List userInputs = null) + { + if (string.IsNullOrEmpty(password)) + { + return null; + } + var globalUserInputs = new List + { + "bitwarden", + "bit", + "warden" + }; + if (userInputs != null && userInputs.Any()) + { + globalUserInputs.AddRange(userInputs); + } + // Use a hash set to get rid of any duplicate user inputs + var hashSet = new HashSet(globalUserInputs); + var finalUserInputs = new string[hashSet.Count]; + hashSet.CopyTo(finalUserInputs); + var result = Zxcvbn.Core.EvaluatePassword(password, finalUserInputs); + return result; + } + + public void NormalizeOptions(PasswordGenerationOptions options, + PasswordGeneratorPolicyOptions enforcedPolicyOptions) + { + options.MinLowercase = 0; + options.MinUppercase = 0; + + if (!options.Uppercase.GetValueOrDefault() && !options.Lowercase.GetValueOrDefault() && + !options.Number.GetValueOrDefault() && !options.Special.GetValueOrDefault()) + { + options.Lowercase = true; + } + + var length = options.Length.GetValueOrDefault(); + if (length < 5) + { + options.Length = 5; + } + else if (length > 128) + { + options.Length = 128; + } + + if (options.Length < enforcedPolicyOptions.MinLength) + { + options.Length = enforcedPolicyOptions.MinLength; + } + + if (options.MinNumber == null) + { + options.MinNumber = 0; + } + else if (options.MinNumber > options.Length) + { + options.MinNumber = options.Length; + } + else if (options.MinNumber > 9) + { + options.MinNumber = 9; + } + + if (options.MinNumber < enforcedPolicyOptions.NumberCount) + { + options.MinNumber = enforcedPolicyOptions.NumberCount; + } + + if (options.MinSpecial == null) + { + options.MinSpecial = 0; + } + else if (options.MinSpecial > options.Length) + { + options.MinSpecial = options.Length; + } + else if (options.MinSpecial > 9) + { + options.MinSpecial = 9; + } + + if (options.MinSpecial < enforcedPolicyOptions.SpecialCount) + { + options.MinSpecial = enforcedPolicyOptions.SpecialCount; + } + + if (options.MinSpecial + options.MinNumber > options.Length) + { + options.MinSpecial = options.Length - options.MinNumber; + } + + if (options.NumWords == null || options.Length < 3) + { + options.NumWords = 3; + } + else if (options.NumWords > 20) + { + options.NumWords = 20; + } + + if (options.NumWords < enforcedPolicyOptions.MinNumberOfWords) + { + options.NumWords = enforcedPolicyOptions.MinNumberOfWords; + } + + if (options.WordSeparator != null && options.WordSeparator.Length > 1) + { + options.WordSeparator = options.WordSeparator[0].ToString(); + } + + SanitizePasswordLength(options, false); + } + + // Helpers + + private async Task> EncryptHistoryAsync(List history) + { + if (!history?.Any() ?? true) + { + return new List(); + } + var tasks = history.Select(async item => + { + if (item == null) + { + return null; + } + var encrypted = await _cryptoService.EncryptAsync(item.Password); + if (encrypted == null) + { + return null; + } + return new GeneratedPasswordHistory + { + Password = encrypted.EncryptedString, + Date = item.Date + }; + }); + var h = await Task.WhenAll(tasks); + return h.Where(x => x != null).ToList(); + } + + private async Task> DecryptHistoryAsync(List history) + { + if (!history?.Any() ?? true) + { + return new List(); + } + var tasks = history.Select(async item => + { + var decrypted = await _cryptoService.DecryptToUtf8Async(new EncString(item.Password)); + return new GeneratedPasswordHistory + { + Password = decrypted, + Date = item.Date + }; + }); + var h = await Task.WhenAll(tasks); + return h.ToList(); + } + + private bool MatchesPrevious(string password, List history) + { + if (!history?.Any() ?? true) + { + return false; + } + return history.Last().Password == password; + } + + private string Capitalize(string str) + { + return str.First().ToString().ToUpper() + str.Substring(1); + } + + private async Task AppendRandomNumberToRandomWordAsync(List wordList) + { + if (wordList == null || wordList.Count <= 0) + { + return; + } + var index = await _cryptoService.RandomNumberAsync(0, wordList.Count - 1); + var num = await _cryptoService.RandomNumberAsync(0, 9); + wordList[index] = wordList[index] + num; + } + + private void SanitizePasswordLength(PasswordGenerationOptions options, bool forGeneration) + { + var minUppercaseCalc = 0; + var minLowercaseCalc = 0; + var minNumberCalc = options.MinNumber; + var minSpecialCalc = options.MinNumber; + + if (options.Uppercase.GetValueOrDefault() && options.MinUppercase.GetValueOrDefault() <= 0) + { + minUppercaseCalc = 1; + } + else if (!options.Uppercase.GetValueOrDefault()) + { + minUppercaseCalc = 0; + } + + if (options.Lowercase.GetValueOrDefault() && options.MinLowercase.GetValueOrDefault() <= 0) + { + minLowercaseCalc = 1; + } + else if (!options.Lowercase.GetValueOrDefault()) + { + minLowercaseCalc = 0; + } + + if (options.Number.GetValueOrDefault() && options.MinNumber.GetValueOrDefault() <= 0) + { + minNumberCalc = 1; + } + else if (!options.Number.GetValueOrDefault()) + { + minNumberCalc = 0; + } + + if (options.Special.GetValueOrDefault() && options.MinSpecial.GetValueOrDefault() <= 0) + { + minSpecialCalc = 1; + } + else if (!options.Special.GetValueOrDefault()) + { + minSpecialCalc = 0; + } + + // This should never happen but is a final safety net + if (options.Length.GetValueOrDefault() < 1) + { + options.Length = 10; + } + + var minLength = minUppercaseCalc + minLowercaseCalc + minNumberCalc + minSpecialCalc; + // Normalize and Generation both require this modification + if (options.Length < minLength) + { + options.Length = minLength; + } + + // Apply other changes if the options object passed in is for generation + if (forGeneration) + { + options.MinUppercase = minUppercaseCalc; + options.MinLowercase = minLowercaseCalc; + options.MinNumber = minNumberCalc; + options.MinSpecial = minSpecialCalc; + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/PclCryptoFunctionService.cs b/src/Maui/Bitwarden/Core/Services/PclCryptoFunctionService.cs new file mode 100644 index 000000000..50c555065 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/PclCryptoFunctionService.cs @@ -0,0 +1,312 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using PCLCrypto; +using static PCLCrypto.WinRTCrypto; + +namespace Bit.Core.Services +{ + public class PclCryptoFunctionService : ICryptoFunctionService + { + private readonly ICryptoPrimitiveService _cryptoPrimitiveService; + + public PclCryptoFunctionService(ICryptoPrimitiveService cryptoPrimitiveService) + { + _cryptoPrimitiveService = cryptoPrimitiveService; + } + + public Task Pbkdf2Async(string password, string salt, CryptoHashAlgorithm algorithm, int iterations) + { + password = NormalizePassword(password); + return Pbkdf2Async(Encoding.UTF8.GetBytes(password), Encoding.UTF8.GetBytes(salt), algorithm, iterations); + } + + public Task Pbkdf2Async(byte[] password, string salt, CryptoHashAlgorithm algorithm, int iterations) + { + return Pbkdf2Async(password, Encoding.UTF8.GetBytes(salt), algorithm, iterations); + } + + public Task Pbkdf2Async(string password, byte[] salt, CryptoHashAlgorithm algorithm, int iterations) + { + password = NormalizePassword(password); + return Pbkdf2Async(Encoding.UTF8.GetBytes(password), salt, algorithm, iterations); + } + + public Task Pbkdf2Async(byte[] password, byte[] salt, CryptoHashAlgorithm algorithm, int iterations) + { + if (algorithm != CryptoHashAlgorithm.Sha256 && algorithm != CryptoHashAlgorithm.Sha512) + { + throw new ArgumentException("Unsupported PBKDF2 algorithm."); + } + return Task.FromResult(_cryptoPrimitiveService.Pbkdf2(password, salt, algorithm, iterations)); + } + + public Task Argon2Async(string password, string salt, int iterations, int memory, int parallelism) + { + password = NormalizePassword(password); + return Argon2Async(Encoding.UTF8.GetBytes(password), Encoding.UTF8.GetBytes(salt), iterations, memory, parallelism); + } + + public Task Argon2Async(byte[] password, string salt, int iterations, int memory, int parallelism) + { + return Argon2Async(password, Encoding.UTF8.GetBytes(salt), iterations, memory, parallelism); + } + + public Task Argon2Async(string password, byte[] salt, int iterations, int memory, int parallelism) + { + password = NormalizePassword(password); + return Argon2Async(Encoding.UTF8.GetBytes(password), salt, iterations, memory, parallelism); + } + + public Task Argon2Async(byte[] password, byte[] salt, int iterations, int memory, int parallelism) + { + return Task.FromResult(_cryptoPrimitiveService.Argon2id(password, salt, iterations, memory, parallelism)); + } + + public async Task HkdfAsync(byte[] ikm, string salt, string info, int outputByteSize, HkdfAlgorithm algorithm) => + await HkdfAsync(ikm, Encoding.UTF8.GetBytes(salt), Encoding.UTF8.GetBytes(info), outputByteSize, algorithm); + + public async Task HkdfAsync(byte[] ikm, byte[] salt, string info, int outputByteSize, HkdfAlgorithm algorithm) => + await HkdfAsync(ikm, salt, Encoding.UTF8.GetBytes(info), outputByteSize, algorithm); + + public async Task HkdfAsync(byte[] ikm, string salt, byte[] info, int outputByteSize, HkdfAlgorithm algorithm) => + await HkdfAsync(ikm, Encoding.UTF8.GetBytes(salt), info, outputByteSize, algorithm); + + public async Task HkdfAsync(byte[] ikm, byte[] salt, byte[] info, int outputByteSize, HkdfAlgorithm algorithm) + { + var prk = await HmacAsync(ikm, salt, HkdfAlgorithmToCryptoHashAlgorithm(algorithm)); + return await HkdfExpandAsync(prk, info, outputByteSize, algorithm); + } + + public async Task HkdfExpandAsync(byte[] prk, string info, int outputByteSize, HkdfAlgorithm algorithm) => + await HkdfExpandAsync(prk, Encoding.UTF8.GetBytes(info), outputByteSize, algorithm); + + // ref: https://tools.ietf.org/html/rfc5869 + public async Task HkdfExpandAsync(byte[] prk, byte[] info, int outputByteSize, HkdfAlgorithm algorithm) + { + var hashLen = algorithm == HkdfAlgorithm.Sha256 ? 32 : 64; + + var maxOutputByteSize = 255 * hashLen; + if (outputByteSize > maxOutputByteSize) + { + throw new ArgumentException($"{nameof(outputByteSize)} is too large. Max is {maxOutputByteSize}, received {outputByteSize}"); + } + if (prk.Length < hashLen) + { + throw new ArgumentException($"{nameof(prk)} length is too small. Must be at least {hashLen} for {algorithm}"); + } + + var cryptoHashAlgorithm = HkdfAlgorithmToCryptoHashAlgorithm(algorithm); + var previousT = new byte[0]; + var runningOkmLength = 0; + var n = (int)Math.Ceiling((double)outputByteSize / hashLen); + var okm = new byte[n * hashLen]; + for (var i = 0; i < n; i++) + { + var t = new byte[previousT.Length + info.Length + 1]; + previousT.CopyTo(t, 0); + info.CopyTo(t, previousT.Length); + t[t.Length - 1] = (byte)(i + 1); + previousT = await HmacAsync(t, prk, cryptoHashAlgorithm); + previousT.CopyTo(okm, runningOkmLength); + runningOkmLength = previousT.Length; + if (runningOkmLength >= outputByteSize) + { + break; + } + } + return okm.Take(outputByteSize).ToArray(); + } + + public Task HashAsync(string value, CryptoHashAlgorithm algorithm) + { + return HashAsync(Encoding.UTF8.GetBytes(value), algorithm); + } + + public Task HashAsync(byte[] value, CryptoHashAlgorithm algorithm) + { + var provider = HashAlgorithmProvider.OpenAlgorithm(ToHashAlgorithm(algorithm)); + return Task.FromResult(provider.HashData(value)); + } + + public Task HmacAsync(byte[] value, byte[] key, CryptoHashAlgorithm algorithm) + { + var provider = MacAlgorithmProvider.OpenAlgorithm(ToMacAlgorithm(algorithm)); + var hasher = provider.CreateHash(key); + hasher.Append(value); + return Task.FromResult(hasher.GetValueAndReset()); + } + + public async Task CompareAsync(byte[] a, byte[] b) + { + var provider = MacAlgorithmProvider.OpenAlgorithm(MacAlgorithm.HmacSha256); + var hasher = provider.CreateHash(await RandomBytesAsync(32)); + + hasher.Append(a); + var mac1 = hasher.GetValueAndReset(); + hasher.Append(b); + var mac2 = hasher.GetValueAndReset(); + if (mac1.Length != mac2.Length) + { + return false; + } + + for (int i = 0; i < mac2.Length; i++) + { + if (mac1[i] != mac2[i]) + { + return false; + } + } + + return true; + } + + public Task AesEncryptAsync(byte[] data, byte[] iv, byte[] key) + { + var provider = SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7); + var cryptoKey = provider.CreateSymmetricKey(key); + return Task.FromResult(CryptographicEngine.Encrypt(cryptoKey, data, iv)); + } + + public Task AesDecryptAsync(byte[] data, byte[] iv, byte[] key) + { + var provider = SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7); + var cryptoKey = provider.CreateSymmetricKey(key); + return Task.FromResult(CryptographicEngine.Decrypt(cryptoKey, data, iv)); + } + + public Task RsaEncryptAsync(byte[] data, byte[] publicKey, CryptoHashAlgorithm algorithm) + { + var provider = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(ToAsymmetricAlgorithm(algorithm)); + var cryptoKey = provider.ImportPublicKey(publicKey, + CryptographicPublicKeyBlobType.X509SubjectPublicKeyInfo); + return Task.FromResult(CryptographicEngine.Encrypt(cryptoKey, data)); + } + + public Task RsaDecryptAsync(byte[] data, byte[] privateKey, CryptoHashAlgorithm algorithm) + { + var provider = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(ToAsymmetricAlgorithm(algorithm)); + var cryptoKey = provider.ImportKeyPair(privateKey, CryptographicPrivateKeyBlobType.Pkcs8RawPrivateKeyInfo); + return Task.FromResult(CryptographicEngine.Decrypt(cryptoKey, data)); + } + + public Task RsaExtractPublicKeyAsync(byte[] privateKey) + { + // Have to specify some algorithm + var provider = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(AsymmetricAlgorithm.RsaOaepSha1); + var cryptoKey = provider.ImportKeyPair(privateKey, CryptographicPrivateKeyBlobType.Pkcs8RawPrivateKeyInfo); + return Task.FromResult(cryptoKey.ExportPublicKey(CryptographicPublicKeyBlobType.X509SubjectPublicKeyInfo)); + } + + public Task> RsaGenerateKeyPairAsync(int length) + { + if (length != 1024 && length != 2048 && length != 4096) + { + throw new ArgumentException("Invalid key pair length."); + } + + // Have to specify some algorithm + var provider = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(AsymmetricAlgorithm.RsaOaepSha1); + var cryptoKey = provider.CreateKeyPair(length); + var publicKey = cryptoKey.ExportPublicKey(CryptographicPublicKeyBlobType.X509SubjectPublicKeyInfo); + var privateKey = cryptoKey.Export(CryptographicPrivateKeyBlobType.Pkcs8RawPrivateKeyInfo); + return Task.FromResult(new Tuple(publicKey, privateKey)); + } + + public Task RandomBytesAsync(int length) + { + return Task.FromResult(CryptographicBuffer.GenerateRandom(length)); + } + + public byte[] RandomBytes(int length) + { + return CryptographicBuffer.GenerateRandom(length); + } + + public Task RandomNumberAsync() + { + return Task.FromResult(CryptographicBuffer.GenerateRandomNumber()); + } + + public uint RandomNumber() + { + return CryptographicBuffer.GenerateRandomNumber(); + } + + private HashAlgorithm ToHashAlgorithm(CryptoHashAlgorithm algorithm) + { + switch (algorithm) + { + case CryptoHashAlgorithm.Sha1: + return HashAlgorithm.Sha1; + case CryptoHashAlgorithm.Sha256: + return HashAlgorithm.Sha256; + case CryptoHashAlgorithm.Sha512: + return HashAlgorithm.Sha512; + case CryptoHashAlgorithm.Md5: + return HashAlgorithm.Md5; + default: + throw new ArgumentException("Unsupported hash algorithm."); + } + } + + private MacAlgorithm ToMacAlgorithm(CryptoHashAlgorithm algorithm) + { + switch (algorithm) + { + case CryptoHashAlgorithm.Sha1: + return MacAlgorithm.HmacSha1; + case CryptoHashAlgorithm.Sha256: + return MacAlgorithm.HmacSha256; + case CryptoHashAlgorithm.Sha512: + return MacAlgorithm.HmacSha512; + default: + throw new ArgumentException("Unsupported mac algorithm."); + } + } + + private AsymmetricAlgorithm ToAsymmetricAlgorithm(CryptoHashAlgorithm algorithm) + { + switch (algorithm) + { + case CryptoHashAlgorithm.Sha1: + return AsymmetricAlgorithm.RsaOaepSha1; + // RsaOaepSha256 is not supported on iOS + // ref: https://github.com/AArnott/PCLCrypto/issues/124 + // case CryptoHashAlgorithm.SHA256: + // return AsymmetricAlgorithm.RsaOaepSha256; + default: + throw new ArgumentException("Unsupported asymmetric algorithm."); + } + } + + // Some users like to copy/paste passwords from external files. Sometimes this can lead to two different + // values on mobiles apps vs the web. For example, on Android an EditText will accept a new line character + // (\n), whereas whenever you paste a new line character on the web in a HTML input box it is converted + // to a space ( ). Normalize those values so that they are the same on all platforms. + private string NormalizePassword(string password) + { + return password + .Replace("\r\n", " ") // Windows-style new line => space + .Replace("\n", " ") // New line => space + .Replace(" ", " "); // No-break space (00A0) => space + } + + private CryptoHashAlgorithm HkdfAlgorithmToCryptoHashAlgorithm(HkdfAlgorithm hkdfAlgorithm) + { + switch (hkdfAlgorithm) + { + case HkdfAlgorithm.Sha256: + return CryptoHashAlgorithm.Sha256; + case HkdfAlgorithm.Sha512: + return CryptoHashAlgorithm.Sha512; + default: + throw new ArgumentException($"Invalid hkdf algorithm type, {hkdfAlgorithm}"); + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/PolicyService.cs b/src/Maui/Bitwarden/Core/Services/PolicyService.cs new file mode 100644 index 000000000..9301763ac --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/PolicyService.cs @@ -0,0 +1,362 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Services +{ + public class PolicyService : IPolicyService + { + private readonly IStateService _stateService; + private readonly IOrganizationService _organizationService; + + private IEnumerable _policyCache; + + public PolicyService( + IStateService stateService, + IOrganizationService organizationService) + { + _stateService = stateService; + _organizationService = organizationService; + } + + public void ClearCache() + { + _policyCache = null; + } + + public async Task> GetAll(PolicyType? type, string userId = null) + { + if (_policyCache == null) + { + var policies = await _stateService.GetEncryptedPoliciesAsync(userId); + if (policies == null) + { + return null; + } + _policyCache = policies.Select(p => new Policy(policies[p.Key])); + } + + if (type != null) + { + return _policyCache.Where(p => p.Type == type).ToList(); + } + + return _policyCache; + } + + public async Task Replace(Dictionary policies, string userId = null) + { + await _stateService.SetEncryptedPoliciesAsync(policies, userId); + _policyCache = null; + + var vaultTimeoutPolicy = policies.FirstOrDefault(p => p.Value.Type == PolicyType.MaximumVaultTimeout); + if (vaultTimeoutPolicy.Value != null) + { + await UpdateVaultTimeoutFromPolicyAsync(new Policy(vaultTimeoutPolicy.Value)); + } + } + + public async Task ClearAsync(string userId) + { + await _stateService.SetEncryptedPoliciesAsync(null, userId); + _policyCache = null; + } + + public async Task UpdateVaultTimeoutFromPolicyAsync(Policy policy, string userId = null) + { + var policyTimeout = policy.GetInt(Policy.MINUTES_KEY); + if (policyTimeout != null) + { + var vaultTimeout = await _stateService.GetVaultTimeoutAsync(userId); + var timeout = vaultTimeout.HasValue ? Math.Min(vaultTimeout.Value, policyTimeout.Value) : policyTimeout.Value; + if (timeout < 0) + { + timeout = policyTimeout.Value; + } + if (vaultTimeout != timeout) + { + await _stateService.SetVaultTimeoutAsync(timeout, userId); + } + } + + var policyAction = policy.GetString(Policy.ACTION_KEY); + if (!string.IsNullOrEmpty(policyAction)) + { + var vaultTimeoutAction = await _stateService.GetVaultTimeoutActionAsync(userId); + var action = policyAction == Policy.ACTION_LOCK ? VaultTimeoutAction.Lock : VaultTimeoutAction.Logout; + if (vaultTimeoutAction != action) + { + await _stateService.SetVaultTimeoutActionAsync(action, userId); + } + } + } + + public async Task GetMasterPasswordPolicyOptions( + IEnumerable policies = null, string userId = null) + { + if (policies == null) + { + policies = await GetAll(PolicyType.MasterPassword, userId); + if (policies == null) + { + return null; + } + } + else + { + policies = policies.Where(p => p.Type == PolicyType.MasterPassword); + } + + policies = policies.Where(p => p.Enabled && p.Data != null); + + if (!policies.Any()) + { + return null; + } + + var enforcedOptions = new MasterPasswordPolicyOptions(); + + foreach (var currentPolicy in policies) + { + var minComplexity = currentPolicy.GetInt("minComplexity"); + if (minComplexity > enforcedOptions.MinComplexity) + { + enforcedOptions.MinComplexity = minComplexity.Value; + } + + var minLength = currentPolicy.GetInt("minLength"); + if (minLength > enforcedOptions.MinLength) + { + enforcedOptions.MinLength = minLength.Value; + } + + if (currentPolicy.GetBool("requireUpper") == true) + { + enforcedOptions.RequireUpper = true; + } + + if (currentPolicy.GetBool("requireLower") == true) + { + enforcedOptions.RequireLower = true; + } + + if (currentPolicy.GetBool("requireNumbers") == true) + { + enforcedOptions.RequireNumbers = true; + } + + if (currentPolicy.GetBool("requireSpecial") == true) + { + enforcedOptions.RequireSpecial = true; + } + + var enforceOnLogin = currentPolicy.GetBool("enforceOnLogin"); + if (enforceOnLogin == true) + { + enforcedOptions.EnforceOnLogin = true; + } + } + + return enforcedOptions; + } + + public async Task EvaluateMasterPassword(int passwordStrength, string newPassword, + MasterPasswordPolicyOptions enforcedPolicyOptions) + { + if (enforcedPolicyOptions == null) + { + return true; + } + + if (enforcedPolicyOptions.MinComplexity > 0 && enforcedPolicyOptions.MinComplexity > passwordStrength) + { + return false; + } + + if (enforcedPolicyOptions.MinLength > 0 && enforcedPolicyOptions.MinLength > newPassword.Length) + { + return false; + } + + if (enforcedPolicyOptions.RequireUpper && newPassword.ToLower() == newPassword) + { + return false; + } + + if (enforcedPolicyOptions.RequireLower && newPassword.ToUpper() == newPassword) + { + return false; + } + + if (enforcedPolicyOptions.RequireNumbers && !newPassword.Any(char.IsDigit)) + { + return false; + } + + if (enforcedPolicyOptions.RequireSpecial && !Regex.IsMatch(newPassword, "^.*[!@#$%\\^&*].*$")) + { + return false; + } + + return true; + } + + public Tuple GetResetPasswordPolicyOptions(IEnumerable policies, + string orgId) + { + var resetPasswordPolicyOptions = new ResetPasswordPolicyOptions(); + + if (policies == null || orgId == null) + { + return new Tuple(resetPasswordPolicyOptions, false); + } + + var policy = policies.FirstOrDefault(p => + p.OrganizationId == orgId && p.Type == PolicyType.ResetPassword && p.Enabled); + resetPasswordPolicyOptions.AutoEnrollEnabled = policy.GetBool("autoEnrollEnabled") ?? false; + + return new Tuple(resetPasswordPolicyOptions, policy != null); + } + + public async Task PolicyAppliesToUser(PolicyType policyType, Func policyFilter = null, + string userId = null) + { + var policies = await GetAll(policyType, userId); + if (policies == null) + { + return false; + } + var organizations = await _organizationService.GetAllAsync(userId); + + IEnumerable filteredPolicies; + + if (policyFilter != null) + { + filteredPolicies = policies.Where(p => p.Enabled && policyFilter(p)); + } + else + { + filteredPolicies = policies.Where(p => p.Enabled); + } + + var policySet = new HashSet(filteredPolicies.Select(p => p.OrganizationId)); + + return organizations.Any(o => + o.Enabled && + o.Status >= OrganizationUserStatusType.Accepted && + o.UsePolicies && + !isExcemptFromPolicies(o, policyType) && + policySet.Contains(o.Id)); + } + + private bool isExcemptFromPolicies(Organization organization, PolicyType policyType) + { + if (policyType == PolicyType.MaximumVaultTimeout) + { + return organization.Type == OrganizationUserType.Owner; + } + + return organization.isExemptFromPolicies; + } + + public async Task ShouldShowVaultFilterAsync() + { + var personalOwnershipPolicyApplies = await PolicyAppliesToUser(PolicyType.PersonalOwnership); + var singleOrgPolicyApplies = await PolicyAppliesToUser(PolicyType.OnlyOrg); + if (personalOwnershipPolicyApplies && singleOrgPolicyApplies) + { + return false; + } + var organizations = await _organizationService.GetAllAsync(); + return organizations?.Any() ?? false; + } + + public async Task GetPasswordGeneratorPolicyOptionsAsync() + { + var policies = await GetAll(PolicyType.PasswordGenerator); + if (policies == null) + { + return null; + } + + var actualPolicies = policies.Where(p => p.Enabled && p.Data != null); + if (!actualPolicies.Any()) + { + return null; + } + + var enforcedOptions = new PasswordGeneratorPolicyOptions(); + + foreach (var currentPolicy in actualPolicies) + { + var defaultType = currentPolicy.GetString("defaultType"); + if (defaultType != null && enforcedOptions.DefaultType != PasswordGenerationOptions.TYPE_PASSWORD) + { + enforcedOptions.DefaultType = defaultType; + } + + var minLength = currentPolicy.GetInt("minLength"); + if (minLength > enforcedOptions.MinLength) + { + enforcedOptions.MinLength = minLength.Value; + } + + if (currentPolicy.GetBool("useUpper") == true) + { + enforcedOptions.UseUppercase = true; + } + + if (currentPolicy.GetBool("useLower") == true) + { + enforcedOptions.UseLowercase = true; + } + + if (currentPolicy.GetBool("useNumbers") == true) + { + enforcedOptions.UseNumbers = true; + } + + var minNumbers = currentPolicy.GetInt("minNumbers"); + if (minNumbers > enforcedOptions.NumberCount) + { + enforcedOptions.NumberCount = minNumbers.Value; + } + + if (currentPolicy.GetBool("useSpecial") == true) + { + enforcedOptions.UseSpecial = true; + } + + var minSpecial = currentPolicy.GetInt("minSpecial"); + if (minSpecial > enforcedOptions.SpecialCount) + { + enforcedOptions.SpecialCount = minSpecial.Value; + } + + var minNumberWords = currentPolicy.GetInt("minNumberWords"); + if (minNumberWords > enforcedOptions.MinNumberOfWords) + { + enforcedOptions.MinNumberOfWords = minNumberWords.Value; + } + + if (currentPolicy.GetBool("capitalize") == true) + { + enforcedOptions.Capitalize = true; + } + + if (currentPolicy.GetBool("includeNumber") == true) + { + enforcedOptions.IncludeNumber = true; + } + } + + return enforcedOptions; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/SearchService.cs b/src/Maui/Bitwarden/Core/Services/SearchService.cs new file mode 100644 index 000000000..0c0bbfe17 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/SearchService.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; + +namespace Bit.Core.Services +{ + public class SearchService : ISearchService + { + private readonly ICipherService _cipherService; + private readonly ISendService _sendService; + + public SearchService( + ICipherService cipherService, + ISendService sendService) + { + _cipherService = cipherService; + _sendService = sendService; + } + + public void ClearIndex() + { + // TODO + } + + public bool IsSearchable(string query) + { + return (query?.Length ?? 0) > 1; + } + + public Task IndexCiphersAsync() + { + // TODO + return Task.FromResult(0); + } + + public async Task> SearchCiphersAsync(string query, Func filter = null, + List ciphers = null, CancellationToken ct = default) + { + var results = new List(); + if (query != null) + { + query = query.Trim().ToLower(); + } + if (query == string.Empty) + { + query = null; + } + if (ciphers == null) + { + ciphers = await _cipherService.GetAllDecryptedAsync(); + } + + ct.ThrowIfCancellationRequested(); + if (filter != null) + { + ciphers = ciphers.Where(filter).ToList(); + } + + ct.ThrowIfCancellationRequested(); + if (!IsSearchable(query)) + { + return ciphers; + } + + return SearchCiphersBasic(ciphers, query); + // TODO: advanced searching with index + } + + public List SearchCiphersBasic(List ciphers, string query, + CancellationToken ct = default, bool deleted = false) + { + ct.ThrowIfCancellationRequested(); + var matchedCiphers = new List(); + var lowPriorityMatchedCiphers = new List(); + query = query.Trim().ToLower().RemoveDiacritics(); + + foreach (var c in ciphers) + { + ct.ThrowIfCancellationRequested(); + if (c.Name?.ToLower().RemoveDiacritics().Contains(query) ?? false) + { + matchedCiphers.Add(c); + } + else if (query.Length >= 8 && c.Id.StartsWith(query)) + { + lowPriorityMatchedCiphers.Add(c); + } + else if (c.SubTitle?.ToLower().RemoveDiacritics().Contains(query) ?? false) + { + lowPriorityMatchedCiphers.Add(c); + } + else if (c.Login?.Uri?.ToLower()?.Contains(query) ?? false) + { + lowPriorityMatchedCiphers.Add(c); + } + } + + ct.ThrowIfCancellationRequested(); + matchedCiphers.AddRange(lowPriorityMatchedCiphers); + return matchedCiphers; + } + + public async Task> SearchSendsAsync(string query, Func filter = null, + List sends = null, CancellationToken ct = default) + { + var results = new List(); + if (query != null) + { + query = query.Trim().ToLower(); + } + if (query == string.Empty) + { + query = null; + } + if (sends == null) + { + sends = await _sendService.GetAllDecryptedAsync(); + } + + ct.ThrowIfCancellationRequested(); + if (filter != null) + { + sends = sends.Where(filter).ToList(); + } + + ct.ThrowIfCancellationRequested(); + if (!IsSearchable(query)) + { + return sends; + } + + return SearchSendsBasic(sends, query); + } + + public List SearchSendsBasic(List sends, string query, CancellationToken ct = default, + bool deleted = false) + { + var matchedSends = new List(); + var lowPriorityMatchSends = new List(); + ct.ThrowIfCancellationRequested(); + query = query.Trim().ToLower(); + + foreach (var s in sends) + { + ct.ThrowIfCancellationRequested(); + if (s.Name?.ToLower().Contains(query) ?? false) + { + matchedSends.Add(s); + } + else if (s.Text?.Text?.ToLower().Contains(query) ?? false) + { + lowPriorityMatchSends.Add(s); + } + else if (s.File?.FileName?.ToLower()?.Contains(query) ?? false) + { + lowPriorityMatchSends.Add(s); + } + } + + ct.ThrowIfCancellationRequested(); + matchedSends.AddRange(lowPriorityMatchSends); + return matchedSends; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/SendService.cs b/src/Maui/Bitwarden/Core/Services/SendService.cs new file mode 100644 index 000000000..b59fad7e6 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/SendService.cs @@ -0,0 +1,300 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; +using Bit.Core.Models.Response; +using Bit.Core.Models.View; +using Newtonsoft.Json; + +namespace Bit.Core.Services +{ + public class SendService : ISendService + { + private List _decryptedSendsCache; + private readonly ICryptoService _cryptoService; + private readonly IStateService _stateService; + private readonly IApiService _apiService; + private readonly II18nService _i18nService; + private readonly ICryptoFunctionService _cryptoFunctionService; + private Task> _getAllDecryptedTask; + private readonly IFileUploadService _fileUploadService; + + public SendService( + ICryptoService cryptoService, + IStateService stateService, + IApiService apiService, + IFileUploadService fileUploadService, + II18nService i18nService, + ICryptoFunctionService cryptoFunctionService) + { + _cryptoService = cryptoService; + _stateService = stateService; + _apiService = apiService; + _fileUploadService = fileUploadService; + _i18nService = i18nService; + _cryptoFunctionService = cryptoFunctionService; + } + + public async Task ClearAsync(string userId) + { + await _stateService.SetEncryptedSendsAsync(null, userId); + ClearCache(); + } + + public void ClearCache() => _decryptedSendsCache = null; + + public async Task DeleteAsync(params string[] ids) + { + var sends = await _stateService.GetEncryptedSendsAsync(); + + if (sends == null) + { + return; + } + + foreach (var id in ids) + { + sends.Remove(id); + } + + await _stateService.SetEncryptedSendsAsync(sends); + ClearCache(); + } + + public async Task DeleteWithServerAsync(string id) + { + await _apiService.DeleteSendAsync(id); + await DeleteAsync(id); + } + + public async Task<(Send send, EncByteArray encryptedFileData)> EncryptAsync(SendView model, byte[] fileData, + string password, SymmetricCryptoKey key = null) + { + if (model.Key == null) + { + model.Key = _cryptoFunctionService.RandomBytes(16); + model.CryptoKey = await _cryptoService.MakeSendKeyAsync(model.Key); + } + + var send = new Send + { + Id = model.Id, + Type = model.Type, + Disabled = model.Disabled, + DeletionDate = model.DeletionDate, + ExpirationDate = model.ExpirationDate, + MaxAccessCount = model.MaxAccessCount, + Key = await _cryptoService.EncryptAsync(model.Key, key), + Name = await _cryptoService.EncryptAsync(model.Name, model.CryptoKey), + Notes = await _cryptoService.EncryptAsync(model.Notes, model.CryptoKey), + HideEmail = model.HideEmail + }; + EncByteArray encryptedFileData = null; + + if (password != null) + { + var passwordHash = await _cryptoFunctionService.Pbkdf2Async(password, model.Key, + CryptoHashAlgorithm.Sha256, 100000); + send.Password = Convert.ToBase64String(passwordHash); + } + + switch (send.Type) + { + case SendType.Text: + send.Text = new SendText + { + Text = await _cryptoService.EncryptAsync(model.Text.Text, model.CryptoKey), + Hidden = model.Text.Hidden + }; + break; + case SendType.File: + send.File = new SendFile(); + if (fileData != null) + { + send.File.FileName = await _cryptoService.EncryptAsync(model.File.FileName, model.CryptoKey); + encryptedFileData = await _cryptoService.EncryptToBytesAsync(fileData, model.CryptoKey); + } + break; + default: + break; + } + + return (send, encryptedFileData); + } + + public async Task> GetAllAsync() + { + var sends = await _stateService.GetEncryptedSendsAsync(); + return sends?.Select(kvp => new Send(kvp.Value)).ToList() ?? new List(); + } + + public async Task> GetAllDecryptedAsync() + { + if (_decryptedSendsCache != null) + { + return _decryptedSendsCache; + } + + var hasKey = await _cryptoService.HasKeyAsync(); + if (!hasKey) + { + throw new Exception("No Key."); + } + + if (_getAllDecryptedTask != null && !_getAllDecryptedTask.IsCompleted && !_getAllDecryptedTask.IsFaulted) + { + return await _getAllDecryptedTask; + } + + async Task> doTask() + { + var decSends = new List(); + + async Task decryptAndAddSendAsync(Send send) => decSends.Add(await send.DecryptAsync()); + await Task.WhenAll((await GetAllAsync()).Select(s => decryptAndAddSendAsync(s))); + + decSends = decSends.OrderBy(s => s, new SendLocaleComparer(_i18nService)).ToList(); + _decryptedSendsCache = decSends; + return _decryptedSendsCache; + } + + _getAllDecryptedTask = doTask(); + return await _getAllDecryptedTask; + } + + public async Task GetAsync(string id) + { + var sends = await _stateService.GetEncryptedSendsAsync(); + + if (sends == null || !sends.ContainsKey(id)) + { + return null; + } + + return new Send(sends[id]); + } + + public async Task ReplaceAsync(Dictionary sends) + { + await _stateService.SetEncryptedSendsAsync(sends); + _decryptedSendsCache = null; + } + + public async Task SaveWithServerAsync(Send send, EncByteArray encryptedFileData) + { + var request = new SendRequest(send, encryptedFileData?.Buffer?.LongLength); + SendResponse response = default; + if (send.Id == null) + { + switch (send.Type) + { + case SendType.Text: + response = await _apiService.PostSendAsync(request); + break; + case SendType.File: + try + { + var uploadDataResponse = await _apiService.PostFileTypeSendAsync(request); + response = uploadDataResponse.SendResponse; + + await _fileUploadService.UploadSendFileAsync(uploadDataResponse, send.File.FileName, encryptedFileData); + } + catch (ApiException e) when (e.Error.StatusCode == HttpStatusCode.NotFound) + { + response = await LegacyServerSendFileUpload(request, send, encryptedFileData); + } + catch + { + if (response != default) + { + await _apiService.DeleteSendAsync(response.Id); + } + throw; + } + break; + default: + throw new NotImplementedException($"Cannot save unknown Send type {send.Type}"); + } + send.Id = response.Id; + } + else + { + response = await _apiService.PutSendAsync(send.Id, request); + } + + var userId = await _stateService.GetActiveUserIdAsync(); + await UpsertAsync(new SendData(response, userId)); + return response.Id; + } + + [Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")] + private async Task LegacyServerSendFileUpload(SendRequest request, Send send, EncByteArray encryptedFileData) + { + var fd = new MultipartFormDataContent($"--BWMobileFormBoundary{DateTime.UtcNow.Ticks}") + { + { new StringContent(JsonConvert.SerializeObject(request)), "model" }, + { new ByteArrayContent(encryptedFileData.Buffer), "data", send.File.FileName.EncryptedString } + }; + + return await _apiService.PostSendFileAsync(fd); + } + + public async Task UpsertAsync(params SendData[] sends) + { + var knownSends = await _stateService.GetEncryptedSendsAsync() ?? + new Dictionary(); + + foreach (var send in sends) + { + knownSends[send.Id] = send; + } + + await _stateService.SetEncryptedSendsAsync(knownSends); + _decryptedSendsCache = null; + } + + public async Task RemovePasswordWithServerAsync(string id) + { + var response = await _apiService.PutSendRemovePasswordAsync(id); + var userId = await _stateService.GetActiveUserIdAsync(); + await UpsertAsync(new SendData(response, userId)); + } + + private class SendLocaleComparer : IComparer + { + private readonly II18nService _i18nService; + + public SendLocaleComparer(II18nService i18nService) + { + _i18nService = i18nService; + } + + public int Compare(SendView a, SendView b) + { + var aName = a?.Name; + var bName = b?.Name; + if (aName == null && bName != null) + { + return -1; + } + if (aName != null && bName == null) + { + return 1; + } + if (aName == null && bName == null) + { + return 0; + } + return _i18nService.StringComparer.Compare(aName, bName); + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/SettingsService.cs b/src/Maui/Bitwarden/Core/Services/SettingsService.cs new file mode 100644 index 000000000..8fc72cfec --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/SettingsService.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Newtonsoft.Json.Linq; + +namespace Bit.Core.Services +{ + public class SettingsService : ISettingsService + { + private const string Keys_EquivalentDomains = "equivalentDomains"; + + private readonly IStateService _stateService; + + private Dictionary _settingsCache; + + public SettingsService( + IStateService stateService) + { + _stateService = stateService; + } + + public void ClearCache() + { + _settingsCache?.Clear(); + _settingsCache = null; + } + + public async Task>> GetEquivalentDomainsAsync() + { + var settings = await GetSettingsAsync(); + if (settings != null && settings.ContainsKey(Keys_EquivalentDomains)) + { + var jArray = (settings[Keys_EquivalentDomains] as JArray); + return jArray?.ToObject>>() ?? new List>(); + } + return new List>(); + } + + public Task SetEquivalentDomainsAsync(List> equivalentDomains) + { + return SetSettingsKeyAsync(Keys_EquivalentDomains, equivalentDomains); + } + + public async Task ClearAsync(string userId) + { + await _stateService.SetSettingsAsync(null, userId); + ClearCache(); + } + + // Helpers + + private async Task> GetSettingsAsync() + { + if (_settingsCache == null) + { + _settingsCache = await _stateService.GetSettingsAsync(); + } + return _settingsCache; + } + + private async Task SetSettingsKeyAsync(string key, T value) + { + var settings = await GetSettingsAsync(); + if (settings == null) + { + settings = new Dictionary(); + } + if (settings.ContainsKey(key)) + { + settings[key] = value; + } + else + { + settings.Add(key, value); + } + await _stateService.SetSettingsAsync(settings); + _settingsCache = settings; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/StateMigrationService.cs b/src/Maui/Bitwarden/Core/Services/StateMigrationService.cs new file mode 100644 index 000000000..caf693bd4 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/StateMigrationService.cs @@ -0,0 +1,616 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; + +namespace Bit.Core.Services +{ + public class StateMigrationService : IStateMigrationService + { + private const int StateVersion = 5; + + private readonly DeviceType _deviceType; + private readonly IStorageService _preferencesStorageService; + private readonly IStorageService _liteDbStorageService; + private readonly IStorageService _secureStorageService; + private readonly SemaphoreSlim _semaphore; + + private enum Storage + { + LiteDb, + Prefs, + Secure, + } + + public StateMigrationService(DeviceType deviceType, IStorageService liteDbStorageService, + IStorageService preferenceStorageService, IStorageService secureStorageService) + { + _deviceType = deviceType; + _liteDbStorageService = liteDbStorageService; + _preferencesStorageService = preferenceStorageService; + _secureStorageService = secureStorageService; + + _semaphore = new SemaphoreSlim(1); + } + + public async Task MigrateIfNeededAsync() + { + await _semaphore.WaitAsync(); + try + { + if (await IsMigrationNeededAsync()) + { + await PerformMigrationAsync(); + } + } + finally + { + _semaphore.Release(); + } + } + + private async Task IsMigrationNeededAsync() + { + var lastVersion = await GetLastStateVersionAsync(); + if (lastVersion == 0) + { + // fresh install, set current/latest version for availability going forward + lastVersion = StateVersion; + await SetLastStateVersionAsync(lastVersion); + } + return lastVersion < StateVersion; + } + + private async Task PerformMigrationAsync() + { + var lastVersion = await GetLastStateVersionAsync(); + switch (lastVersion) + { + case 1: + await MigrateFrom1To2Async(); + goto case 2; + case 2: + await MigrateFrom2To3Async(); + goto case 3; + case 3: + await MigrateFrom3To4Async(); + break; + case 4: + await MigrateFrom4To5Async(); + break; + } + } + + #region v1 to v2 Migration + + private class V1Keys + { + internal const string EnvironmentUrlsKey = "environmentUrls"; + } + + private async Task MigrateFrom1To2Async() + { + // move environmentUrls from LiteDB to prefs + var environmentUrls = await GetValueAsync(Storage.LiteDb, V1Keys.EnvironmentUrlsKey); + if (environmentUrls == null) + { + throw new Exception("'environmentUrls' must be in LiteDB during migration from 1 to 2"); + } + await SetValueAsync(Storage.Prefs, V2Keys.EnvironmentUrlsKey, environmentUrls); + + // Update stored version + await SetLastStateVersionAsync(2); + + // Remove old data + await RemoveValueAsync(Storage.LiteDb, V1Keys.EnvironmentUrlsKey); + } + + #endregion + + #region v2 to v3 Migration + + private class V2Keys + { + internal const string SyncOnRefreshKey = "syncOnRefresh"; + internal const string VaultTimeoutKey = "lockOption"; + internal const string VaultTimeoutActionKey = "vaultTimeoutAction"; + internal const string LastActiveTimeKey = "lastActiveTime"; + internal const string BiometricUnlockKey = "fingerprintUnlock"; + internal const string ProtectedPin = "protectedPin"; + internal const string PinProtectedKey = "pinProtectedKey"; + internal const string DefaultUriMatch = "defaultUriMatch"; + internal const string DisableAutoTotpCopyKey = "disableAutoTotpCopy"; + internal const string EnvironmentUrlsKey = "environmentUrls"; + internal const string AutofillDisableSavePromptKey = "autofillDisableSavePrompt"; + internal const string AutofillBlacklistedUrisKey = "autofillBlacklistedUris"; + internal const string DisableFaviconKey = "disableFavicon"; + internal const string ThemeKey = "theme"; + internal const string ClearClipboardKey = "clearClipboard"; + internal const string PreviousPageKey = "previousPage"; + internal const string InlineAutofillEnabledKey = "inlineAutofillEnabled"; + internal const string InvalidUnlockAttempts = "invalidUnlockAttempts"; + internal const string PasswordRepromptAutofillKey = "passwordRepromptAutofillKey"; + internal const string PasswordVerifiedAutofillKey = "passwordVerifiedAutofillKey"; + internal const string MigratedFromV1 = "migratedFromV1"; + internal const string MigratedFromV1AutofillPromptShown = "migratedV1AutofillPromptShown"; + internal const string TriedV1Resync = "triedV1Resync"; + internal const string Keys_UserId = "userId"; + internal const string Keys_UserEmail = "userEmail"; + internal const string Keys_Stamp = "securityStamp"; + internal const string Keys_Kdf = "kdf"; + internal const string Keys_KdfIterations = "kdfIterations"; + internal const string Keys_EmailVerified = "emailVerified"; + internal const string Keys_ForcePasswordReset = "forcePasswordReset"; + internal const string Keys_AccessToken = "accessToken"; + internal const string Keys_RefreshToken = "refreshToken"; + internal const string Keys_LocalData = "ciphersLocalData"; + internal const string Keys_NeverDomains = "neverDomains"; + internal const string Keys_Key = "key"; + internal const string Keys_EncOrgKeys = "encOrgKeys"; + internal const string Keys_EncPrivateKey = "encPrivateKey"; + internal const string Keys_EncKey = "encKey"; + internal const string Keys_KeyHash = "keyHash"; + internal const string Keys_UsesKeyConnector = "usesKeyConnector"; + internal const string Keys_PassGenOptions = "passwordGenerationOptions"; + internal const string Keys_PassGenHistory = "generatedPasswordHistory"; + } + + private async Task MigrateFrom2To3Async() + { + // build account and state + var userId = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_UserId); + var email = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_UserEmail); + string name = null; + var hasPremiumPersonally = false; + var accessToken = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_AccessToken); + if (!string.IsNullOrWhiteSpace(accessToken)) + { + var tokenService = ServiceContainer.Resolve("tokenService"); + await tokenService.SetAccessTokenAsync(accessToken, true); + + if (string.IsNullOrWhiteSpace(userId)) + { + userId = tokenService.GetUserId(); + } + if (string.IsNullOrWhiteSpace(email)) + { + email = tokenService.GetEmail(); + } + name = tokenService.GetName(); + hasPremiumPersonally = tokenService.GetPremium(); + } + if (string.IsNullOrWhiteSpace(userId)) + { + throw new Exception("'userId' must be in LiteDB during migration from 2 to 3"); + } + + var kdfType = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_Kdf); + var kdfIterations = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_KdfIterations); + var stamp = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_Stamp); + var emailVerified = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_EmailVerified); + var refreshToken = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_RefreshToken); + var account = new Account( + new Account.AccountProfile() + { + UserId = userId, + Email = email, + Name = name, + Stamp = stamp, + KdfType = (KdfType?)kdfType, + KdfIterations = kdfIterations, + EmailVerified = emailVerified, + HasPremiumPersonally = hasPremiumPersonally, + }, + new Account.AccountTokens() + { + AccessToken = accessToken, + RefreshToken = refreshToken, + } + ); + var environmentUrls = await GetValueAsync(Storage.Prefs, V2Keys.EnvironmentUrlsKey); + var vaultTimeout = await GetValueAsync(Storage.Prefs, V2Keys.VaultTimeoutKey); + var vaultTimeoutAction = await GetValueAsync(Storage.Prefs, V2Keys.VaultTimeoutActionKey); + account.Settings = new Account.AccountSettings() + { + EnvironmentUrls = environmentUrls, + VaultTimeout = vaultTimeout, + VaultTimeoutAction = + vaultTimeoutAction == "logout" ? VaultTimeoutAction.Logout : VaultTimeoutAction.Lock + }; + var state = new State { Accounts = new Dictionary { [userId] = account } }; + state.ActiveUserId = userId; + await SetValueAsync(Storage.LiteDb, Constants.StateKey, state); + + // migrate user-specific non-state data + var syncOnRefresh = await GetValueAsync(Storage.LiteDb, V2Keys.SyncOnRefreshKey); + await SetValueAsync(Storage.LiteDb, V3Keys.SyncOnRefreshKey(userId), syncOnRefresh); + var lastActiveTime = await GetValueAsync(Storage.Prefs, V2Keys.LastActiveTimeKey); + await SetValueAsync(Storage.LiteDb, V3Keys.LastActiveTimeKey(userId), lastActiveTime); + var biometricUnlock = await GetValueAsync(Storage.LiteDb, V2Keys.BiometricUnlockKey); + await SetValueAsync(Storage.LiteDb, V3Keys.BiometricUnlockKey(userId), biometricUnlock); + var protectedPin = await GetValueAsync(Storage.LiteDb, V2Keys.ProtectedPin); + await SetValueAsync(Storage.LiteDb, V3Keys.ProtectedPinKey(userId), protectedPin); + var pinProtectedKey = await GetValueAsync(Storage.LiteDb, V2Keys.PinProtectedKey); + await SetValueAsync(Storage.LiteDb, V3Keys.PinProtectedKey(userId), pinProtectedKey); + var defaultUriMatch = await GetValueAsync(Storage.Prefs, V2Keys.DefaultUriMatch); + await SetValueAsync(Storage.LiteDb, V3Keys.DefaultUriMatchKey(userId), defaultUriMatch); + var disableAutoTotpCopy = await GetValueAsync(Storage.Prefs, V2Keys.DisableAutoTotpCopyKey); + await SetValueAsync(Storage.LiteDb, V3Keys.DisableAutoTotpCopyKey(userId), disableAutoTotpCopy); + var autofillDisableSavePrompt = + await GetValueAsync(Storage.Prefs, V2Keys.AutofillDisableSavePromptKey); + await SetValueAsync(Storage.LiteDb, V3Keys.AutofillDisableSavePromptKey(userId), + autofillDisableSavePrompt); + var autofillBlacklistedUris = + await GetValueAsync>(Storage.LiteDb, V2Keys.AutofillBlacklistedUrisKey); + await SetValueAsync(Storage.LiteDb, V3Keys.AutofillBlacklistedUrisKey(userId), autofillBlacklistedUris); + var disableFavicon = await GetValueAsync(Storage.Prefs, V2Keys.DisableFaviconKey); + await SetValueAsync(Storage.LiteDb, V3Keys.DisableFaviconKey(userId), disableFavicon); + var theme = await GetValueAsync(Storage.Prefs, V2Keys.ThemeKey); + await SetValueAsync(Storage.LiteDb, V3Keys.ThemeKey(userId), theme); + var clearClipboard = await GetValueAsync(Storage.Prefs, V2Keys.ClearClipboardKey); + await SetValueAsync(Storage.LiteDb, V3Keys.ClearClipboardKey(userId), clearClipboard); + var previousPage = await GetValueAsync(Storage.LiteDb, V2Keys.PreviousPageKey); + await SetValueAsync(Storage.LiteDb, V3Keys.PreviousPageKey(userId), previousPage); + var inlineAutofillEnabled = await GetValueAsync(Storage.Prefs, V2Keys.InlineAutofillEnabledKey); + await SetValueAsync(Storage.LiteDb, V3Keys.InlineAutofillEnabledKey(userId), inlineAutofillEnabled); + var invalidUnlockAttempts = await GetValueAsync(Storage.Prefs, V2Keys.InvalidUnlockAttempts); + await SetValueAsync(Storage.LiteDb, V3Keys.InvalidUnlockAttemptsKey(userId), invalidUnlockAttempts); + var passwordRepromptAutofill = + await GetValueAsync(Storage.LiteDb, V2Keys.PasswordRepromptAutofillKey); + await SetValueAsync(Storage.LiteDb, V3Keys.PasswordRepromptAutofillKey(userId), + passwordRepromptAutofill); + var passwordVerifiedAutofill = + await GetValueAsync(Storage.LiteDb, V2Keys.PasswordVerifiedAutofillKey); + await SetValueAsync(Storage.LiteDb, V3Keys.PasswordVerifiedAutofillKey(userId), + passwordVerifiedAutofill); + var cipherLocalData = await GetValueAsync>>(Storage.LiteDb, + V2Keys.Keys_LocalData); + await SetValueAsync(Storage.LiteDb, V3Keys.LocalDataKey(userId), cipherLocalData); + var neverDomains = await GetValueAsync>(Storage.LiteDb, V2Keys.Keys_NeverDomains); + await SetValueAsync(Storage.LiteDb, V3Keys.NeverDomainsKey(userId), neverDomains); + var key = await GetValueAsync(Storage.Secure, V2Keys.Keys_Key); + await SetValueAsync(Storage.Secure, V3Keys.KeyKey(userId), key); + var encOrgKeys = await GetValueAsync>(Storage.LiteDb, V2Keys.Keys_EncOrgKeys); + await SetValueAsync(Storage.LiteDb, V3Keys.EncOrgKeysKey(userId), encOrgKeys); + var encPrivateKey = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_EncPrivateKey); + await SetValueAsync(Storage.LiteDb, V3Keys.EncPrivateKeyKey(userId), encPrivateKey); + var encKey = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_EncKey); + await SetValueAsync(Storage.LiteDb, V3Keys.EncKeyKey(userId), encKey); + var keyHash = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_KeyHash); + await SetValueAsync(Storage.LiteDb, V3Keys.KeyHashKey(userId), keyHash); + var usesKeyConnector = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_UsesKeyConnector); + await SetValueAsync(Storage.LiteDb, V3Keys.UsesKeyConnectorKey(userId), usesKeyConnector); + var passGenOptions = + await GetValueAsync(Storage.LiteDb, V2Keys.Keys_PassGenOptions); + await SetValueAsync(Storage.LiteDb, V3Keys.PassGenOptionsKey(userId), passGenOptions); + var passGenHistory = + await GetValueAsync>(Storage.LiteDb, V2Keys.Keys_PassGenHistory); + await SetValueAsync(Storage.LiteDb, V3Keys.PassGenHistoryKey(userId), passGenHistory); + + // migrate global non-state data + await SetValueAsync(Storage.Prefs, V3Keys.PreAuthEnvironmentUrlsKey, environmentUrls); + + // Update stored version + await SetLastStateVersionAsync(3); + + // Remove old data + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_UserId); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_UserEmail); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_AccessToken); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_RefreshToken); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_Kdf); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_KdfIterations); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_Stamp); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_EmailVerified); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_ForcePasswordReset); + await RemoveValueAsync(Storage.Prefs, V2Keys.EnvironmentUrlsKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.PinProtectedKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.VaultTimeoutKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.VaultTimeoutActionKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.SyncOnRefreshKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.LastActiveTimeKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.BiometricUnlockKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.ProtectedPin); + await RemoveValueAsync(Storage.Prefs, V2Keys.DefaultUriMatch); + await RemoveValueAsync(Storage.Prefs, V2Keys.DisableAutoTotpCopyKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.AutofillDisableSavePromptKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.AutofillBlacklistedUrisKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.DisableFaviconKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.ThemeKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.ClearClipboardKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.PreviousPageKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.InlineAutofillEnabledKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.InvalidUnlockAttempts); + await RemoveValueAsync(Storage.LiteDb, V2Keys.PasswordRepromptAutofillKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.PasswordVerifiedAutofillKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.MigratedFromV1); + await RemoveValueAsync(Storage.Prefs, V2Keys.MigratedFromV1AutofillPromptShown); + await RemoveValueAsync(Storage.Prefs, V2Keys.TriedV1Resync); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_LocalData); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_NeverDomains); + await RemoveValueAsync(Storage.Secure, V2Keys.Keys_Key); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_EncOrgKeys); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_EncPrivateKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_EncKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_KeyHash); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_UsesKeyConnector); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_PassGenOptions); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_PassGenHistory); + } + + #endregion + + #region v3 to v4 Migration + + private class V3Keys + { + internal const string PreAuthEnvironmentUrlsKey = "preAuthEnvironmentUrls"; + internal static string LocalDataKey(string userId) => $"ciphersLocalData_{userId}"; + internal static string NeverDomainsKey(string userId) => $"neverDomains_{userId}"; + internal static string KeyKey(string userId) => $"key_{userId}"; + internal static string EncOrgKeysKey(string userId) => $"encOrgKeys_{userId}"; + internal static string EncPrivateKeyKey(string userId) => $"encPrivateKey_{userId}"; + internal static string EncKeyKey(string userId) => $"encKey_{userId}"; + internal static string KeyHashKey(string userId) => $"keyHash_{userId}"; + internal static string PinProtectedKey(string userId) => $"pinProtectedKey_{userId}"; + internal static string PassGenOptionsKey(string userId) => $"passwordGenerationOptions_{userId}"; + internal static string PassGenHistoryKey(string userId) => $"generatedPasswordHistory_{userId}"; + internal static string LastActiveTimeKey(string userId) => $"lastActiveTime_{userId}"; + internal static string InvalidUnlockAttemptsKey(string userId) => $"invalidUnlockAttempts_{userId}"; + internal static string InlineAutofillEnabledKey(string userId) => $"inlineAutofillEnabled_{userId}"; + internal static string AutofillDisableSavePromptKey(string userId) => $"autofillDisableSavePrompt_{userId}"; + internal static string AutofillBlacklistedUrisKey(string userId) => $"autofillBlacklistedUris_{userId}"; + internal static string ClearClipboardKey(string userId) => $"clearClipboard_{userId}"; + internal static string SyncOnRefreshKey(string userId) => $"syncOnRefresh_{userId}"; + internal static string DefaultUriMatchKey(string userId) => $"defaultUriMatch_{userId}"; + internal static string DisableAutoTotpCopyKey(string userId) => $"disableAutoTotpCopy_{userId}"; + internal static string PreviousPageKey(string userId) => $"previousPage_{userId}"; + + internal static string PasswordRepromptAutofillKey(string userId) => + $"passwordRepromptAutofillKey_{userId}"; + + internal static string PasswordVerifiedAutofillKey(string userId) => + $"passwordVerifiedAutofillKey_{userId}"; + + internal static string UsesKeyConnectorKey(string userId) => $"usesKeyConnector_{userId}"; + internal static string ProtectedPinKey(string userId) => $"protectedPin_{userId}"; + internal static string BiometricUnlockKey(string userId) => $"biometricUnlock_{userId}"; + internal static string ThemeKey(string userId) => $"theme_{userId}"; + internal static string AutoDarkThemeKey(string userId) => $"autoDarkTheme_{userId}"; + internal static string DisableFaviconKey(string userId) => $"disableFavicon_{userId}"; + } + + private async Task MigrateFrom3To4Async() + { + var state = await GetValueAsync(Storage.LiteDb, Constants.StateKey); + if (state?.Accounts is null) + { + // Update stored version + await SetLastStateVersionAsync(4); + return; + } + + string firstUserId = null; + + // move values from state to standalone values in LiteDB + foreach (var account in state.Accounts.Where(a => a.Value?.Profile?.UserId != null)) + { + var userId = account.Value.Profile.UserId; + if (firstUserId == null) + { + firstUserId = userId; + } + var vaultTimeout = account.Value.Settings?.VaultTimeout; + await SetValueAsync(Storage.LiteDb, V4Keys.VaultTimeoutKey(userId), vaultTimeout); + + var vaultTimeoutAction = account.Value.Settings?.VaultTimeoutAction; + await SetValueAsync(Storage.LiteDb, V4Keys.VaultTimeoutActionKey(userId), vaultTimeoutAction); + + var screenCaptureAllowed = account.Value.Settings?.ScreenCaptureAllowed; + await SetValueAsync(Storage.LiteDb, V4Keys.ScreenCaptureAllowedKey(userId), screenCaptureAllowed); + } + + // use values from first userId to apply globals + if (firstUserId != null) + { + var theme = await GetValueAsync(Storage.LiteDb, V3Keys.ThemeKey(firstUserId)); + await SetValueAsync(Storage.LiteDb, V4Keys.ThemeKey, theme); + + var autoDarkTheme = await GetValueAsync(Storage.LiteDb, V3Keys.AutoDarkThemeKey(firstUserId)); + await SetValueAsync(Storage.LiteDb, V4Keys.AutoDarkThemeKey, autoDarkTheme); + + var disableFavicon = await GetValueAsync(Storage.LiteDb, V3Keys.DisableFaviconKey(firstUserId)); + await SetValueAsync(Storage.LiteDb, V4Keys.DisableFaviconKey, disableFavicon); + } + + // Update stored version + await SetLastStateVersionAsync(4); + + // Remove old data + foreach (var account in state.Accounts) + { + var userId = account.Value?.Profile?.UserId; + if (userId != null) + { + await RemoveValueAsync(Storage.LiteDb, V3Keys.ThemeKey(userId)); + await RemoveValueAsync(Storage.LiteDb, V3Keys.AutoDarkThemeKey(userId)); + await RemoveValueAsync(Storage.LiteDb, V3Keys.DisableFaviconKey(userId)); + } + } + + // Removal of old state data will happen organically as state is rebuilt in app + } + + #endregion + + #region v4 to v5 Migration + + private class V4Keys + { + internal static string VaultTimeoutKey(string userId) => $"vaultTimeout_{userId}"; + internal static string VaultTimeoutActionKey(string userId) => $"vaultTimeoutAction_{userId}"; + internal static string ScreenCaptureAllowedKey(string userId) => $"screenCaptureAllowed_{userId}"; + internal const string ThemeKey = "theme"; + internal const string AutoDarkThemeKey = "autoDarkTheme"; + internal const string DisableFaviconKey = "disableFavicon"; + internal const string BiometricIntegrityKey = "biometricIntegrityState"; + internal const string iOSAutoFillBiometricIntegrityKey = "iOSAutoFillBiometricIntegrityState"; + internal const string iOSExtensionBiometricIntegrityKey = "iOSExtensionBiometricIntegrityState"; + internal const string iOSShareExtensionBiometricIntegrityKey = "iOSShareExtensionBiometricIntegrityState"; + } + + private async Task MigrateFrom4To5Async() + { + var bioIntegrityState = await GetValueAsync(Storage.Prefs, V4Keys.BiometricIntegrityKey); + var iOSAutofillBioIntegrityState = + await GetValueAsync(Storage.Prefs, V4Keys.iOSAutoFillBiometricIntegrityKey); + var iOSExtensionBioIntegrityState = + await GetValueAsync(Storage.Prefs, V4Keys.iOSExtensionBiometricIntegrityKey); + var iOSShareExtensionBioIntegrityState = + await GetValueAsync(Storage.Prefs, V4Keys.iOSShareExtensionBiometricIntegrityKey); + + if (_deviceType == DeviceType.Android && string.IsNullOrWhiteSpace(bioIntegrityState)) + { + bioIntegrityState = Guid.NewGuid().ToString(); + } + + await SetValueAsync(Storage.Prefs, V5Keys.BiometricIntegritySourceKey, bioIntegrityState); + + if (_deviceType == DeviceType.iOS) + { + await SetValueAsync(Storage.Prefs, V5Keys.iOSAutoFillBiometricIntegritySourceKey, + iOSAutofillBioIntegrityState); + await SetValueAsync(Storage.Prefs, V5Keys.iOSExtensionBiometricIntegritySourceKey, + iOSExtensionBioIntegrityState); + await SetValueAsync(Storage.Prefs, V5Keys.iOSShareExtensionBiometricIntegritySourceKey, + iOSShareExtensionBioIntegrityState); + } + + var state = await GetValueAsync(Storage.LiteDb, Constants.StateKey); + if (state?.Accounts is null) + { + // No accounts available, update stored version and exit + await SetLastStateVersionAsync(5); + return; + } + + // build integrity keys for existing users + foreach (var account in state.Accounts.Where(a => a.Value?.Profile?.UserId != null)) + { + var userId = account.Value.Profile.UserId; + + await SetValueAsync(Storage.LiteDb, + V5Keys.AccountBiometricIntegrityValidKey(userId, bioIntegrityState), true); + + if (_deviceType == DeviceType.iOS) + { + await SetValueAsync(Storage.LiteDb, + V5Keys.AccountBiometricIntegrityValidKey(userId, iOSAutofillBioIntegrityState), true); + await SetValueAsync(Storage.LiteDb, + V5Keys.AccountBiometricIntegrityValidKey(userId, iOSExtensionBioIntegrityState), true); + await SetValueAsync(Storage.LiteDb, + V5Keys.AccountBiometricIntegrityValidKey(userId, iOSShareExtensionBioIntegrityState), true); + } + } + + // Update stored version + await SetLastStateVersionAsync(5); + + // Remove old data + await RemoveValueAsync(Storage.Prefs, V4Keys.BiometricIntegrityKey); + await RemoveValueAsync(Storage.Prefs, V4Keys.iOSAutoFillBiometricIntegrityKey); + await RemoveValueAsync(Storage.Prefs, V4Keys.iOSExtensionBiometricIntegrityKey); + await RemoveValueAsync(Storage.Prefs, V4Keys.iOSShareExtensionBiometricIntegrityKey); + } + + private class V5Keys + { + internal const string BiometricIntegritySourceKey = "biometricIntegritySource"; + internal const string iOSAutoFillBiometricIntegritySourceKey = "iOSAutoFillBiometricIntegritySource"; + internal const string iOSExtensionBiometricIntegritySourceKey = "iOSExtensionBiometricIntegritySource"; + + internal const string iOSShareExtensionBiometricIntegritySourceKey = + "iOSShareExtensionBiometricIntegritySource"; + + internal static string AccountBiometricIntegrityValidKey(string userId, string systemBioIntegrityState) => + $"accountBiometricIntegrityValid_{userId}_{systemBioIntegrityState}"; + } + + #endregion + + // Helpers + + private async Task GetLastStateVersionAsync() + { + var lastVersion = await GetValueAsync(Storage.Prefs, Constants.StateVersionKey); + if (lastVersion != null) + { + return lastVersion.Value; + } + + // check for v1 state + var v1EnvUrls = await GetValueAsync(Storage.LiteDb, V1Keys.EnvironmentUrlsKey); + if (v1EnvUrls != null) + { + // environmentUrls still in LiteDB (never migrated to prefs), this is v1 + return 1; + } + + // check for v2 state + var v2UserId = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_UserId); + if (v2UserId != null) + { + // standalone userId still exists (never moved to Account object), this is v2 + return 2; + } + + // this is a fresh install + return 0; + } + + private async Task SetLastStateVersionAsync(int value) + { + await SetValueAsync(Storage.Prefs, Constants.StateVersionKey, value); + } + + private async Task GetValueAsync(Storage storage, string key) + { + var value = await GetStorageService(storage).GetAsync(key); + return value; + } + + private async Task SetValueAsync(Storage storage, string key, T value) + { + if (value == null) + { + await RemoveValueAsync(storage, key); + return; + } + await GetStorageService(storage).SaveAsync(key, value); + } + + private async Task RemoveValueAsync(Storage storage, string key) + { + await GetStorageService(storage).RemoveAsync(key); + } + + private IStorageService GetStorageService(Storage storage) + { + switch (storage) + { + case Storage.Secure: + return _secureStorageService; + case Storage.Prefs: + return _preferencesStorageService; + default: + return _liteDbStorageService; + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/StateService.cs b/src/Maui/Bitwarden/Core/Services/StateService.cs new file mode 100644 index 000000000..ae2d62d8e --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/StateService.cs @@ -0,0 +1,1660 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Response; +using Bit.Core.Models.View; +using Bit.Core.Utilities; + +namespace Bit.Core.Services +{ + public class StateService : IStateService + { + // TODO: Refactor this removing all storage services and use the IStorageMediatorService instead + + private readonly IStorageService _storageService; + private readonly IStorageService _secureStorageService; + private readonly IStorageMediatorService _storageMediatorService; + private readonly IMessagingService _messagingService; + + private State _state; + private bool _migrationChecked; + + public List AccountViews { get; set; } + + public StateService(IStorageService storageService, + IStorageService secureStorageService, + IStorageMediatorService storageMediatorService, + IMessagingService messagingService) + { + _storageService = storageService; + _secureStorageService = secureStorageService; + _storageMediatorService = storageMediatorService; + _messagingService = messagingService; + } + + public async Task GetActiveUserIdAsync() + { + await CheckStateAsync(); + + var activeUserId = _state?.ActiveUserId; + if (activeUserId == null) + { + var state = await GetStateFromStorageAsync(); + activeUserId = state?.ActiveUserId; + } + return activeUserId; + } + + public async Task GetActiveUserEmailAsync() + { + var activeUserId = await GetActiveUserIdAsync(); + return await GetEmailAsync(activeUserId); + } + + public async Task GetActiveUserCustomDataAsync(Func dataMapper) + { + var userId = await GetActiveUserIdAsync(); + var account = await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ); + return dataMapper(account); + } + + public async Task IsActiveAccountAsync(string userId = null) + { + if (userId == null) + { + return true; + } + return userId == await GetActiveUserIdAsync(); + } + + public async Task SetActiveUserAsync(string userId) + { + if (userId != null) + { + await ValidateUserAsync(userId); + } + await CheckStateAsync(); + var state = await GetStateFromStorageAsync(); + state.ActiveUserId = userId; + await SaveStateToStorageAsync(state); + _state.ActiveUserId = userId; + + // Update pre-auth settings based on now-active user + await SetRememberedOrgIdentifierAsync(await GetRememberedOrgIdentifierAsync()); + await SetPreAuthEnvironmentUrlsAsync(await GetEnvironmentUrlsAsync()); + + await SetLastUserShouldConnectToWatchAsync(); + } + + public async Task CheckExtensionActiveUserAndSwitchIfNeededAsync() + { + var extensionUserId = await GetExtensionActiveUserIdFromStorageAsync(); + if (string.IsNullOrEmpty(extensionUserId)) + { + return; + } + + if (_state?.ActiveUserId == extensionUserId) + { + // Clear the value in case the user changes the active user from the app + // so if that happens and the user sends the app to background and comes back + // the user is not changed again. + await SaveExtensionActiveUserIdToStorageAsync(null); + return; + } + + await SetActiveUserAsync(extensionUserId); + await SaveExtensionActiveUserIdToStorageAsync(null); + _messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT); + } + + public async Task IsAuthenticatedAsync(string userId = null) + { + return await GetAccessTokenAsync(userId) != null; + } + + public async Task GetUserIdAsync(string email) + { + if (string.IsNullOrWhiteSpace(email)) + { + throw new ArgumentNullException(nameof(email)); + } + + await CheckStateAsync(); + if (_state?.Accounts != null) + { + foreach (var account in _state.Accounts) + { + var accountEmail = account.Value?.Profile?.Email; + if (accountEmail == email) + { + return account.Value.Profile.UserId; + } + } + } + return null; + } + + public async Task RefreshAccountViewsAsync(bool allowAddAccountRow) + { + await CheckStateAsync(); + + if (AccountViews == null) + { + AccountViews = new List(); + } + else + { + AccountViews.Clear(); + } + + var accountList = _state?.Accounts?.Values.ToList(); + if (accountList == null) + { + return; + } + var vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); + foreach (var account in accountList) + { + var isActiveAccount = account.Profile.UserId == _state.ActiveUserId; + var accountView = new AccountView(account, isActiveAccount); + + if (await vaultTimeoutService.IsLoggedOutByTimeoutAsync(accountView.UserId) || + await vaultTimeoutService.ShouldLogOutByTimeoutAsync(accountView.UserId)) + { + accountView.AuthStatus = AuthenticationStatus.LoggedOut; + } + else if (await vaultTimeoutService.IsLockedAsync(accountView.UserId) || + await vaultTimeoutService.ShouldLockAsync(accountView.UserId)) + { + accountView.AuthStatus = AuthenticationStatus.Locked; + } + else + { + accountView.AuthStatus = AuthenticationStatus.Unlocked; + } + AccountViews.Add(accountView); + } + if (allowAddAccountRow && AccountViews.Count < Constants.MaxAccounts) + { + AccountViews.Add(new AccountView()); + } + } + + public async Task AddAccountAsync(Account account) + { + await ScaffoldNewAccountAsync(account); + await SetActiveUserAsync(account.Profile.UserId); + await RefreshAccountViewsAsync(true); + } + + public async Task LogoutAccountAsync(string userId, bool userInitiated) + { + if (string.IsNullOrWhiteSpace(userId)) + { + throw new ArgumentNullException(nameof(userId)); + } + + await CheckStateAsync(); + await RemoveAccountAsync(userId, userInitiated); + + // If user initiated logout (not vault timeout) and ActiveUserId is null after account removal, find the + // next user to make active, if any + if (userInitiated && _state?.ActiveUserId == null && _state?.Accounts != null) + { + foreach (var account in _state.Accounts) + { + var uid = account.Value?.Profile?.UserId; + if (uid == null) + { + continue; + } + await SetActiveUserAsync(uid); + break; + } + } + } + + public async Task GetPreAuthEnvironmentUrlsAsync() + { + return await GetValueAsync( + Constants.PreAuthEnvironmentUrlsKey, await GetDefaultStorageOptionsAsync()); + } + + public async Task SetPreAuthEnvironmentUrlsAsync(EnvironmentUrlData value) + { + await SetValueAsync( + Constants.PreAuthEnvironmentUrlsKey, value, await GetDefaultStorageOptionsAsync()); + } + + public async Task GetEnvironmentUrlsAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Settings?.EnvironmentUrls; + } + + public async Task GetBiometricUnlockAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.BiometricUnlockKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetBiometricUnlockAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.BiometricUnlockKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetBiometricLockedAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultInMemoryOptionsAsync()) + ))?.VolatileData?.BiometricLocked ?? true; + } + + public async Task SetBiometricLockedAsync(bool value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultInMemoryOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.VolatileData.BiometricLocked = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetSystemBiometricIntegrityState(string bioIntegritySrcKey) + { + return await GetValueAsync(bioIntegritySrcKey, await GetDefaultStorageOptionsAsync()); + } + + public async Task SetSystemBiometricIntegrityState(string bioIntegritySrcKey, string systemBioIntegrityState) + { + await SetValueAsync(bioIntegritySrcKey, systemBioIntegrityState, await GetDefaultStorageOptionsAsync()); + } + + public async Task IsAccountBiometricIntegrityValidAsync(string bioIntegritySrcKey, string userId = null) + { + var systemBioIntegrityState = await GetSystemBiometricIntegrityState(bioIntegritySrcKey); + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync( + Constants.AccountBiometricIntegrityValidKey(reconciledOptions.UserId, systemBioIntegrityState), + reconciledOptions) ?? false; + } + + public async Task SetAccountBiometricIntegrityValidAsync(string bioIntegritySrcKey, string userId = null) + { + var systemBioIntegrityState = await GetSystemBiometricIntegrityState(bioIntegritySrcKey); + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync( + Constants.AccountBiometricIntegrityValidKey(reconciledOptions.UserId, systemBioIntegrityState), + true, reconciledOptions); + } + + public async Task CanAccessPremiumAsync(string userId = null) + { + if (userId == null) + { + userId = await GetActiveUserIdAsync(); + } + if (!await IsAuthenticatedAsync(userId)) + { + return false; + } + + var account = await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync())); + if (account?.Profile?.HasPremiumPersonally.GetValueOrDefault() ?? false) + { + return true; + } + + var organizationService = ServiceContainer.Resolve("organizationService"); + var organizations = await organizationService.GetAllAsync(userId); + return organizations?.Any(o => o.UsersGetPremium && o.Enabled) ?? false; + } + + public async Task SetPersonalPremiumAsync(bool value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + if (account?.Profile == null || account.Profile.HasPremiumPersonally.GetValueOrDefault() == value) + { + return; + } + + account.Profile.HasPremiumPersonally = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetProtectedPinAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.ProtectedPinKey(reconciledOptions.UserId), reconciledOptions); + } + + public async Task SetProtectedPinAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.ProtectedPinKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetPinProtectedAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.PinProtectedKey(reconciledOptions.UserId), reconciledOptions); + } + + public async Task SetPinProtectedAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.PinProtectedKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetPinProtectedKeyAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultInMemoryOptionsAsync()) + ))?.VolatileData?.PinProtectedKey; + } + + public async Task SetPinProtectedKeyAsync(EncString value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultInMemoryOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.VolatileData.PinProtectedKey = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task SetKdfConfigurationAsync(KdfConfig config, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Profile.KdfType = config.Type; + account.Profile.KdfIterations = config.Iterations; + account.Profile.KdfMemory = config.Memory; + account.Profile.KdfParallelism = config.Parallelism; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetKeyEncryptedAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultSecureStorageOptionsAsync()); + return await GetValueAsync(Constants.KeyKey(reconciledOptions.UserId), reconciledOptions); + } + + public async Task SetKeyEncryptedAsync(string value, string userId) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultSecureStorageOptionsAsync()); + await SetValueAsync(Constants.KeyKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetKeyDecryptedAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultInMemoryOptionsAsync()) + ))?.VolatileData?.Key; + } + + public async Task SetKeyDecryptedAsync(SymmetricCryptoKey value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultInMemoryOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.VolatileData.Key = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetKeyHashAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.KeyHashKey(reconciledOptions.UserId), reconciledOptions); + } + + public async Task SetKeyHashAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.KeyHashKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetEncKeyEncryptedAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.EncKeyKey(reconciledOptions.UserId), reconciledOptions); + } + + public async Task SetEncKeyEncryptedAsync(string value, string userId) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.EncKeyKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task> GetOrgKeysEncryptedAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync>(Constants.EncOrgKeysKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetOrgKeysEncryptedAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.EncOrgKeysKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetPrivateKeyEncryptedAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.EncPrivateKeyKey(reconciledOptions.UserId), reconciledOptions); + } + + public async Task SetPrivateKeyEncryptedAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.EncPrivateKeyKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task> GetAutofillBlacklistedUrisAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync>(Constants.AutofillBlacklistedUrisKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetAutofillBlacklistedUrisAsync(List value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.AutofillBlacklistedUrisKey(reconciledOptions.UserId), value, + reconciledOptions); + } + + public async Task GetAutofillTileAddedAsync() + { + return await GetValueAsync(Constants.AutofillTileAdded, await GetDefaultStorageOptionsAsync()); + } + + public async Task SetAutofillTileAddedAsync(bool? value) + { + await SetValueAsync(Constants.AutofillTileAdded, value, await GetDefaultStorageOptionsAsync()); + } + + public async Task GetEmailAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.Email; + } + + public async Task GetNameAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.Name; + } + + public async Task SetNameAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Profile.Name = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetOrgIdentifierAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.OrgIdentifier; + } + + public async Task GetLastActiveTimeAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.LastActiveTimeKey(reconciledOptions.UserId), reconciledOptions); + } + + public async Task SetLastActiveTimeAsync(long? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.LastActiveTimeKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetVaultTimeoutAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.VaultTimeoutKey(reconciledOptions.UserId), reconciledOptions); + } + + public async Task SetVaultTimeoutAsync(int? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.VaultTimeoutKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetVaultTimeoutActionAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.VaultTimeoutActionKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetVaultTimeoutActionAsync(VaultTimeoutAction? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.VaultTimeoutActionKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetScreenCaptureAllowedAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.ScreenCaptureAllowedKey(reconciledOptions.UserId), + reconciledOptions) ?? CoreHelpers.InDebugMode(); + } + + public async Task SetScreenCaptureAllowedAsync(bool value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.ScreenCaptureAllowedKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetLastFileCacheClearAsync() + { + return await GetValueAsync(Constants.LastFileCacheClearKey, + await GetDefaultStorageOptionsAsync()); + } + + public async Task SetLastFileCacheClearAsync(DateTime? value) + { + await SetValueAsync(Constants.LastFileCacheClearKey, value, await GetDefaultStorageOptionsAsync()); + } + + public async Task GetPreviousPageInfoAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.PreviousPageKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetPreviousPageInfoAsync(PreviousPageInfo value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.PreviousPageKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetInvalidUnlockAttemptsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.InvalidUnlockAttemptsKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetInvalidUnlockAttemptsAsync(int? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.InvalidUnlockAttemptsKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetLastBuildAsync() + { + return await GetValueAsync(Constants.LastBuildKey, await GetDefaultStorageOptionsAsync()); + } + + public async Task SetLastBuildAsync(string value) + { + await SetValueAsync(Constants.LastBuildKey, value, await GetDefaultStorageOptionsAsync()); + } + + public async Task GetDisableFaviconAsync() + { + return await GetValueAsync(Constants.DisableFaviconKey, await GetDefaultStorageOptionsAsync()); + } + + public async Task SetDisableFaviconAsync(bool? value) + { + await SetValueAsync(Constants.DisableFaviconKey, value, await GetDefaultStorageOptionsAsync()); + } + + public async Task GetDisableAutoTotpCopyAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.DisableAutoTotpCopyKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetDisableAutoTotpCopyAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.DisableAutoTotpCopyKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetInlineAutofillEnabledAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.InlineAutofillEnabledKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetInlineAutofillEnabledAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.InlineAutofillEnabledKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetAutofillDisableSavePromptAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.AutofillDisableSavePromptKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetAutofillDisableSavePromptAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.AutofillDisableSavePromptKey(reconciledOptions.UserId), value, + reconciledOptions); + } + + public async Task>> GetLocalDataAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync>>( + Constants.LocalDataKey(reconciledOptions.UserId), reconciledOptions); + } + + public async Task SetLocalDataAsync(Dictionary> value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.LocalDataKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task> GetEncryptedCiphersAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync>(Constants.CiphersKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetEncryptedCiphersAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.CiphersKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetDefaultUriMatchAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.DefaultUriMatchKey(reconciledOptions.UserId), reconciledOptions); + } + + public async Task SetDefaultUriMatchAsync(int? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.DefaultUriMatchKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task> GetNeverDomainsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync>(Constants.NeverDomainsKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetNeverDomainsAsync(HashSet value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.NeverDomainsKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetClearClipboardAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.ClearClipboardKey(reconciledOptions.UserId), reconciledOptions); + } + + public async Task SetClearClipboardAsync(int? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.ClearClipboardKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task> GetEncryptedCollectionsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync>( + Constants.CollectionsKey(reconciledOptions.UserId), reconciledOptions); + } + + public async Task SetEncryptedCollectionsAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.CollectionsKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetPasswordRepromptAutofillAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.PasswordRepromptAutofillKey(reconciledOptions.UserId), + reconciledOptions) ?? false; + } + + public async Task SetPasswordRepromptAutofillAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.PasswordRepromptAutofillKey(reconciledOptions.UserId), value, + reconciledOptions); + } + + public async Task GetPasswordVerifiedAutofillAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.PasswordVerifiedAutofillKey(reconciledOptions.UserId), + reconciledOptions) ?? false; + } + + public async Task SetPasswordVerifiedAutofillAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.PasswordVerifiedAutofillKey(reconciledOptions.UserId), value, + reconciledOptions); + } + + public async Task GetLastSyncAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.LastSyncKey(reconciledOptions.UserId), reconciledOptions); + } + + public async Task SetLastSyncAsync(DateTime? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.LastSyncKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetSecurityStampAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.Stamp; + } + + public async Task SetSecurityStampAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Profile.Stamp = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetEmailVerifiedAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.EmailVerified ?? false; + } + + public async Task SetEmailVerifiedAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Profile.EmailVerified = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetSyncOnRefreshAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.SyncOnRefreshKey(reconciledOptions.UserId), + reconciledOptions) ?? false; + } + + public async Task SetSyncOnRefreshAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.SyncOnRefreshKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetRememberedEmailAsync() + { + return await GetValueAsync(Constants.RememberedEmailKey, await GetDefaultStorageOptionsAsync()); + } + + public async Task SetRememberedEmailAsync(string value) + { + await SetValueAsync(Constants.RememberedEmailKey, value, await GetDefaultStorageOptionsAsync()); + } + + public async Task GetRememberedOrgIdentifierAsync() + { + return await GetValueAsync(Constants.RememberedOrgIdentifierKey, + await GetDefaultStorageOptionsAsync()); + } + + public async Task SetRememberedOrgIdentifierAsync(string value) + { + await SetValueAsync(Constants.RememberedOrgIdentifierKey, value, await GetDefaultStorageOptionsAsync()); + } + + public async Task GetThemeAsync() + { + return await GetValueAsync(Constants.ThemeKey, await GetDefaultStorageOptionsAsync()); + } + + public async Task SetThemeAsync(string value) + { + await SetValueAsync(Constants.ThemeKey, value, await GetDefaultStorageOptionsAsync()); + } + + public async Task GetAutoDarkThemeAsync() + { + return await GetValueAsync(Constants.AutoDarkThemeKey, await GetDefaultStorageOptionsAsync()); + } + + public async Task SetAutoDarkThemeAsync(string value) + { + await SetValueAsync(Constants.AutoDarkThemeKey, value, await GetDefaultStorageOptionsAsync()); + } + + public string GetLocale() + { + return _storageMediatorService.Get(Constants.AppLocaleKey); + } + + public void SetLocale(string locale) + { + _storageMediatorService.Save(Constants.AppLocaleKey, locale); + } + + public async Task GetAddSitePromptShownAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.AddSitePromptShownKey, reconciledOptions); + } + + public async Task SetAddSitePromptShownAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.AddSitePromptShownKey, value, reconciledOptions); + } + + public async Task GetPushInitialPromptShownAsync() + { + return await GetValueAsync(Constants.PushInitialPromptShownKey, + await GetDefaultStorageOptionsAsync()); + } + + public async Task SetPushInitialPromptShownAsync(bool? value) + { + await SetValueAsync(Constants.PushInitialPromptShownKey, value, await GetDefaultStorageOptionsAsync()); + } + + public async Task GetPushLastRegistrationDateAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.PushLastRegistrationDateKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetPushLastRegistrationDateAsync(DateTime? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.PushLastRegistrationDateKey(reconciledOptions.UserId), value, + reconciledOptions); + } + + public async Task GetPushInstallationRegistrationErrorAsync() + { + return await GetValueAsync(Constants.PushInstallationRegistrationErrorKey, + await GetDefaultStorageOptionsAsync()); + } + + public async Task SetPushInstallationRegistrationErrorAsync(string value) + { + await SetValueAsync(Constants.PushInstallationRegistrationErrorKey, value, + await GetDefaultStorageOptionsAsync()); + } + + public async Task GetPushCurrentTokenAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.PushCurrentTokenKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetPushCurrentTokenAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.PushCurrentTokenKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task> GetEventCollectionAsync() + { + return await GetValueAsync>(Constants.EventCollectionKey, + await GetDefaultStorageOptionsAsync()); + } + + public async Task SetEventCollectionAsync(List value) + { + await SetValueAsync(Constants.EventCollectionKey, value, await GetDefaultStorageOptionsAsync()); + } + + public async Task> GetEncryptedFoldersAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync>(Constants.FoldersKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetEncryptedFoldersAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.FoldersKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task> GetEncryptedPoliciesAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync>(Constants.PoliciesKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetEncryptedPoliciesAsync(Dictionary value, string userId) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.PoliciesKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetPushRegisteredTokenAsync() + { + return await GetValueAsync(Constants.PushRegisteredTokenKey, await GetDefaultStorageOptionsAsync()); + } + + public async Task SetPushRegisteredTokenAsync(string value) + { + await SetValueAsync(Constants.PushRegisteredTokenKey, value, await GetDefaultStorageOptionsAsync()); + } + + public async Task GetUsesKeyConnectorAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.UsesKeyConnectorKey(reconciledOptions.UserId), + reconciledOptions) ?? false; + } + + public async Task SetUsesKeyConnectorAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.UsesKeyConnectorKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetForcePasswordResetReasonAsync(string userId = null) + { + var reconcileOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return (await GetAccountAsync(reconcileOptions))?.Profile?.ForcePasswordResetReason; + } + + public async Task SetForcePasswordResetReasonAsync(ForcePasswordResetReason? value, string userId = null) + { + var reconcileOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconcileOptions); + account.Profile.ForcePasswordResetReason = value; + await SaveAccountAsync(account, reconcileOptions); + } + + public async Task> GetOrganizationsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync>( + Constants.OrganizationsKey(reconciledOptions.UserId), reconciledOptions); + } + + public async Task SetOrganizationsAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.OrganizationsKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetPasswordGenerationOptionsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.PassGenOptionsKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetPasswordGenerationOptionsAsync(PasswordGenerationOptions value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.PassGenOptionsKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetUsernameGenerationOptionsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync( + Constants.UsernameGenOptionsKey(reconciledOptions.UserId), reconciledOptions); + } + + public async Task SetUsernameGenerationOptionsAsync(UsernameGenerationOptions value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.UsernameGenOptionsKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task> GetEncryptedPasswordGenerationHistory(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync>( + Constants.PassGenHistoryKey(reconciledOptions.UserId), reconciledOptions); + } + + public async Task SetEncryptedPasswordGenerationHistoryAsync(List value, + string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.PassGenHistoryKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task> GetEncryptedSendsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync>(Constants.SendsKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetEncryptedSendsAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.SendsKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task> GetSettingsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync>(Constants.SettingsKey(reconciledOptions.UserId), + reconciledOptions); + } + + public async Task SetSettingsAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.SettingsKey(reconciledOptions.UserId), value, reconciledOptions); + } + + public async Task GetAccessTokenAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Tokens?.AccessToken; + } + + public async Task SetAccessTokenAsync(string value, bool skipTokenStorage, string userId = null) + { + var reconciledOptions = ReconcileOptions( + new StorageOptions { UserId = userId, SkipTokenStorage = skipTokenStorage }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Tokens.AccessToken = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetRefreshTokenAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Tokens?.RefreshToken; + } + + public async Task SetRefreshTokenAsync(string value, bool skipTokenStorage, string userId = null) + { + var reconciledOptions = ReconcileOptions( + new StorageOptions { UserId = userId, SkipTokenStorage = skipTokenStorage }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Tokens.RefreshToken = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetTwoFactorTokenAsync(string email = null) + { + var reconciledOptions = + ReconcileOptions(new StorageOptions { Email = email }, await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.TwoFactorTokenKey(reconciledOptions.Email), reconciledOptions); + } + + public async Task SetTwoFactorTokenAsync(string value, string email = null) + { + var reconciledOptions = + ReconcileOptions(new StorageOptions { Email = email }, await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.TwoFactorTokenKey(reconciledOptions.Email), value, reconciledOptions); + } + + public async Task GetApprovePasswordlessLoginsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.ApprovePasswordlessLoginsKey(reconciledOptions.UserId), + reconciledOptions) ?? false; + } + + public async Task SetApprovePasswordlessLoginsAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.ApprovePasswordlessLoginsKey(reconciledOptions.UserId), value, + reconciledOptions); + } + + public async Task GetPasswordlessLoginNotificationAsync() + { + return await GetValueAsync(Constants.PasswordlessLoginNotificationKey, + await GetDefaultStorageOptionsAsync()); + } + + public async Task SetPasswordlessLoginNotificationAsync(PasswordlessRequestNotification value) + { + await SetValueAsync(Constants.PasswordlessLoginNotificationKey, value, + await GetDefaultStorageOptionsAsync()); + } + + public async Task SetAvatarColorAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Profile.AvatarColor = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetAvatarColorAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.AvatarColor; + } + + public async Task GetPreLoginEmailAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + return await GetValueAsync(Constants.PreLoginEmailKey, options); + } + + public async Task SetPreLoginEmailAsync(string value) + { + var options = await GetDefaultStorageOptionsAsync(); + await SetValueAsync(Constants.PreLoginEmailKey, value, options); + } + + public ConfigResponse GetConfigs() + { + return _storageMediatorService.Get(Constants.ConfigsKey); + } + + public void SetConfigs(ConfigResponse value) + { + _storageMediatorService.Save(Constants.ConfigsKey, value); + } + + // Helpers + + [Obsolete("Use IStorageMediatorService instead")] + private async Task GetValueAsync(string key, StorageOptions options) + { + return await GetStorageService(options).GetAsync(key); + } + + [Obsolete("Use IStorageMediatorService instead")] + private async Task SetValueAsync(string key, T value, StorageOptions options) + { + if (value == null) + { + await GetStorageService(options).RemoveAsync(key); + return; + } + await GetStorageService(options).SaveAsync(key, value); + } + + [Obsolete("Use IStorageMediatorService instead")] + private IStorageService GetStorageService(StorageOptions options) + { + return options.UseSecureStorage.GetValueOrDefault(false) ? _secureStorageService : _storageService; + } + + private async Task ComposeKeyAsync(Func userDependantKey, string userId = null) + { + return userDependantKey(userId ?? await GetActiveUserIdAsync()); + } + + private async Task GetAccountAsync(StorageOptions options) + { + await CheckStateAsync(); + + if (options?.UserId == null) + { + return null; + } + + // Memory + if (_state?.Accounts?.ContainsKey(options.UserId) ?? false) + { + if (_state.Accounts[options.UserId].VolatileData == null) + { + _state.Accounts[options.UserId].VolatileData = new Account.AccountVolatileData(); + } + return _state.Accounts[options.UserId]; + } + + // Storage + var state = await GetStateFromStorageAsync(); + if (state?.Accounts?.ContainsKey(options.UserId) ?? false) + { + state.Accounts[options.UserId].VolatileData = new Account.AccountVolatileData(); + return state.Accounts[options.UserId]; + } + + return null; + } + + private async Task SaveAccountAsync(Account account, StorageOptions options = null) + { + if (account?.Profile?.UserId == null) + { + throw new Exception("account?.Profile?.UserId cannot be null"); + } + + await CheckStateAsync(); + + // Memory + if (UseMemory(options)) + { + if (_state.Accounts == null) + { + _state.Accounts = new Dictionary(); + } + _state.Accounts[account.Profile.UserId] = account; + } + + // Storage + if (UseDisk(options)) + { + var state = await GetStateFromStorageAsync() ?? new State(); + if (state.Accounts == null) + { + state.Accounts = new Dictionary(); + } + + // Use Account copy constructor to clone with keys excluded (for storage) + state.Accounts[account.Profile.UserId] = new Account(account); + + // If we have a vault timeout and the action is log out, don't store token + if (options?.SkipTokenStorage.GetValueOrDefault() ?? false) + { + state.Accounts[account.Profile.UserId].Tokens.AccessToken = null; + state.Accounts[account.Profile.UserId].Tokens.RefreshToken = null; + } + + await SaveStateToStorageAsync(state); + } + } + + private async Task RemoveAccountAsync(string userId, bool userInitiated) + { + if (string.IsNullOrWhiteSpace(userId)) + { + throw new ArgumentNullException(nameof(userId)); + } + + var email = await GetEmailAsync(userId); + + // Memory + if (_state?.Accounts?.ContainsKey(userId) ?? false) + { + if (userInitiated) + { + _state.Accounts.Remove(userId); + } + else + { + _state.Accounts[userId].Tokens.AccessToken = null; + _state.Accounts[userId].Tokens.RefreshToken = null; + _state.Accounts[userId].VolatileData = null; + } + } + if (userInitiated && _state?.ActiveUserId == userId) + { + _state.ActiveUserId = null; + } + + // Storage + var stateModified = false; + var state = await GetStateFromStorageAsync(); + if (state?.Accounts?.ContainsKey(userId) ?? false) + { + if (userInitiated) + { + state.Accounts.Remove(userId); + } + else + { + state.Accounts[userId].Tokens.AccessToken = null; + state.Accounts[userId].Tokens.RefreshToken = null; + } + stateModified = true; + } + if (userInitiated && state?.ActiveUserId == userId) + { + state.ActiveUserId = null; + stateModified = true; + } + if (stateModified) + { + await SaveStateToStorageAsync(state); + } + + // Non-state storage + await SetProtectedPinAsync(null, userId); + await SetPinProtectedAsync(null, userId); + await SetKeyEncryptedAsync(null, userId); + await SetKeyHashAsync(null, userId); + await SetEncKeyEncryptedAsync(null, userId); + await SetOrgKeysEncryptedAsync(null, userId); + await SetPrivateKeyEncryptedAsync(null, userId); + await SetLastActiveTimeAsync(null, userId); + await SetPreviousPageInfoAsync(null, userId); + await SetInvalidUnlockAttemptsAsync(null, userId); + await SetLocalDataAsync(null, userId); + await SetEncryptedCiphersAsync(null, userId); + await SetEncryptedCollectionsAsync(null, userId); + await SetLastSyncAsync(null, userId); + await SetEncryptedFoldersAsync(null, userId); + await SetEncryptedPoliciesAsync(null, userId); + await SetUsesKeyConnectorAsync(null, userId); + await SetOrganizationsAsync(null, userId); + await SetEncryptedPasswordGenerationHistoryAsync(null, userId); + await SetEncryptedSendsAsync(null, userId); + await SetSettingsAsync(null, userId); + await SetApprovePasswordlessLoginsAsync(null, userId); + } + + private async Task ScaffoldNewAccountAsync(Account account) + { + await CheckStateAsync(); + + account.Settings.EnvironmentUrls = await GetPreAuthEnvironmentUrlsAsync(); + + // Storage + var state = await GetStateFromStorageAsync() ?? new State(); + if (state.Accounts == null) + { + state.Accounts = new Dictionary(); + } + + state.Accounts[account.Profile.UserId] = account; + await SaveStateToStorageAsync(state); + + // Memory + if (_state == null) + { + _state = state; + } + else + { + if (_state.Accounts == null) + { + _state.Accounts = new Dictionary(); + } + _state.Accounts[account.Profile.UserId] = account; + } + + // Check if account has logged in before by checking a guaranteed non-null pref + if (await GetVaultTimeoutActionAsync(account.Profile.UserId) == null) + { + // Account has never logged in, set defaults + await SetVaultTimeoutAsync(Constants.VaultTimeoutDefault, account.Profile.UserId); + await SetVaultTimeoutActionAsync(VaultTimeoutAction.Lock, account.Profile.UserId); + } + } + + private StorageOptions ReconcileOptions(StorageOptions requestedOptions, StorageOptions defaultOptions) + { + if (requestedOptions == null) + { + return defaultOptions; + } + requestedOptions.StorageLocation = requestedOptions.StorageLocation ?? defaultOptions.StorageLocation; + requestedOptions.UseSecureStorage = requestedOptions.UseSecureStorage ?? defaultOptions.UseSecureStorage; + requestedOptions.UserId = requestedOptions.UserId ?? defaultOptions.UserId; + requestedOptions.Email = requestedOptions.Email ?? defaultOptions.Email; + requestedOptions.SkipTokenStorage = requestedOptions.SkipTokenStorage ?? defaultOptions.SkipTokenStorage; + return requestedOptions; + } + + /// + /// Gets the default options for storage. + /// If it's only used for composing the constant key with the user id + /// then use instead + /// which saves time if the user id is already known + /// + private async Task GetDefaultStorageOptionsAsync() + { + return new StorageOptions() + { + StorageLocation = StorageLocation.Both, + UserId = await GetActiveUserIdAsync(), + }; + } + + private async Task GetDefaultSecureStorageOptionsAsync() + { + return new StorageOptions() + { + StorageLocation = StorageLocation.Disk, + UseSecureStorage = true, + UserId = await GetActiveUserIdAsync(), + }; + } + + private async Task GetDefaultInMemoryOptionsAsync() + { + return new StorageOptions() + { + StorageLocation = StorageLocation.Memory, + UserId = await GetActiveUserIdAsync(), + }; + } + + private bool UseMemory(StorageOptions options) + { + return options?.StorageLocation == StorageLocation.Memory || + options?.StorageLocation == StorageLocation.Both; + } + + private bool UseDisk(StorageOptions options) + { + return options?.StorageLocation == StorageLocation.Disk || + options?.StorageLocation == StorageLocation.Both; + } + + private async Task GetStateFromStorageAsync() + { + return await _storageService.GetAsync(Constants.StateKey); + } + + private async Task SaveStateToStorageAsync(State state) + { + await _storageService.SaveAsync(Constants.StateKey, state); + } + + private async Task GetExtensionActiveUserIdFromStorageAsync() + { + return await _storageService.GetAsync(Constants.iOSExtensionActiveUserIdKey); + } + + public async Task SaveExtensionActiveUserIdToStorageAsync(string userId) + { + await _storageService.SaveAsync(Constants.iOSExtensionActiveUserIdKey, userId); + } + + private async Task CheckStateAsync() + { + if (!_migrationChecked) + { + var migrationService = ServiceContainer.Resolve(); + await migrationService.MigrateIfNeededAsync(); + _migrationChecked = true; + } + + if (_state == null) + { + _state = await GetStateFromStorageAsync() ?? new State(); + } + } + + private async Task ValidateUserAsync(string userId) + { + if (string.IsNullOrWhiteSpace(userId)) + { + throw new ArgumentNullException(nameof(userId)); + } + await CheckStateAsync(); + var accounts = _state?.Accounts; + if (accounts == null || !accounts.Any()) + { + throw new Exception("At least one account required to validate user"); + } + foreach (var account in accounts) + { + if (account.Key == userId) + { + // found match, user is valid + return; + } + } + throw new Exception("User does not exist in account list"); + } + + public async Task GetShouldConnectToWatchAsync(string userId = null) + { + var reconciledOptions = + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()); + return await GetValueAsync(Constants.ShouldConnectToWatchKey(reconciledOptions.UserId), + reconciledOptions) ?? false; + } + + public async Task SetShouldConnectToWatchAsync(bool shouldConnect, string userId = null) + { + var reconciledOptions = + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()); + await SetValueAsync(Constants.ShouldConnectToWatchKey(reconciledOptions.UserId), shouldConnect, + reconciledOptions); + await SetLastUserShouldConnectToWatchAsync(shouldConnect); + } + + public async Task GetLastUserShouldConnectToWatchAsync() + { + return await GetValueAsync(Constants.LastUserShouldConnectToWatchKey, + await GetDefaultStorageOptionsAsync()) ?? false; + } + + private async Task SetLastUserShouldConnectToWatchAsync(bool? shouldConnect = null) + { + await SetValueAsync(Constants.LastUserShouldConnectToWatchKey, + shouldConnect ?? await GetShouldConnectToWatchAsync(), await GetDefaultStorageOptionsAsync()); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/StorageMediatorService.cs b/src/Maui/Bitwarden/Core/Services/StorageMediatorService.cs new file mode 100644 index 000000000..d54924ecb --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/StorageMediatorService.cs @@ -0,0 +1,62 @@ +using System.Threading.Tasks; +using Bit.Core.Abstractions; + +namespace Bit.Core.Services +{ + public class StorageMediatorService : IStorageMediatorService + { + private readonly IStorageService _storageService; + private readonly IStorageService _secureStorageService; + private readonly ISynchronousStorageService _synchronousStorageService; + + public StorageMediatorService(IStorageService storageService, + IStorageService secureStorageService, + ISynchronousStorageService synchronousStorageService) + { + _storageService = storageService; + _secureStorageService = secureStorageService; + _synchronousStorageService = synchronousStorageService; + } + + public T Get(string key) + { + return _synchronousStorageService.Get(key); + } + + public void Save(string key, T obj) + { + _synchronousStorageService.Save(key, obj); + } + + public void Remove(string key) + { + _synchronousStorageService.Remove(key); + } + + public Task GetAsync(string key, bool useSecureStorage = false) + { + return GetAsyncStorage(useSecureStorage).GetAsync(key); + } + + public async Task SaveAsync(string key, T obj, bool useSecureStorage = false, bool allowSaveNull = false) + { + if (obj is null && !allowSaveNull) + { + await GetAsyncStorage(useSecureStorage).RemoveAsync(key); + return; + } + + await GetAsyncStorage(useSecureStorage).SaveAsync(key, obj); + } + + public Task RemoveAsync(string key, bool useSecureStorage = false) + { + return GetAsyncStorage(useSecureStorage).RemoveAsync(key); + } + + IStorageService GetAsyncStorage(bool useSecureStorage) + { + return useSecureStorage ? _secureStorageService : _storageService; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/SyncService.cs b/src/Maui/Bitwarden/Core/Services/SyncService.cs new file mode 100644 index 000000000..4351e0f08 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/SyncService.cs @@ -0,0 +1,435 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Response; +using Bit.Core.Utilities; + +namespace Bit.Core.Services +{ + public class SyncService : ISyncService + { + private readonly IStateService _stateService; + private readonly IApiService _apiService; + private readonly ISettingsService _settingsService; + private readonly IFolderService _folderService; + private readonly ICipherService _cipherService; + private readonly ICryptoService _cryptoService; + private readonly ICollectionService _collectionService; + private readonly IOrganizationService _organizationService; + private readonly IMessagingService _messagingService; + private readonly IPolicyService _policyService; + private readonly ISendService _sendService; + private readonly IKeyConnectorService _keyConnectorService; + private readonly ILogger _logger; + private readonly Func, Task> _logoutCallbackAsync; + + private readonly LazyResolve _watchDeviceService = new LazyResolve(); + + public SyncService( + IStateService stateService, + IApiService apiService, + ISettingsService settingsService, + IFolderService folderService, + ICipherService cipherService, + ICryptoService cryptoService, + ICollectionService collectionService, + IOrganizationService organizationService, + IMessagingService messagingService, + IPolicyService policyService, + ISendService sendService, + IKeyConnectorService keyConnectorService, + ILogger logger, + Func, Task> logoutCallbackAsync) + { + _stateService = stateService; + _apiService = apiService; + _settingsService = settingsService; + _folderService = folderService; + _cipherService = cipherService; + _cryptoService = cryptoService; + _collectionService = collectionService; + _organizationService = organizationService; + _messagingService = messagingService; + _policyService = policyService; + _sendService = sendService; + _keyConnectorService = keyConnectorService; + _logger = logger; + _logoutCallbackAsync = logoutCallbackAsync; + } + + public bool SyncInProgress { get; set; } + + public async Task GetLastSyncAsync() + { + if (await _stateService.GetActiveUserIdAsync() == null) + { + return null; + } + return await _stateService.GetLastSyncAsync(); + } + + public async Task SetLastSyncAsync(DateTime date) + { + if (await _stateService.GetActiveUserIdAsync() == null) + { + return; + } + await _stateService.SetLastSyncAsync(date); + } + + public async Task FullSyncAsync(bool forceSync, bool allowThrowOnError = false) + { + SyncStarted(); + var isAuthenticated = await _stateService.IsAuthenticatedAsync(); + if (!isAuthenticated) + { + return SyncCompleted(false); + } + var now = DateTime.UtcNow; + var needsSyncResult = await NeedsSyncingAsync(forceSync); + var needsSync = needsSyncResult.Item1; + var skipped = needsSyncResult.Item2; + if (skipped) + { + return SyncCompleted(false); + } + if (!needsSync) + { + await SetLastSyncAsync(now); + return SyncCompleted(false); + } + var userId = await _stateService.GetActiveUserIdAsync(); + try + { + var response = await _apiService.GetSyncAsync(); + await SyncProfileAsync(response.Profile); + await SyncFoldersAsync(userId, response.Folders); + await SyncCollectionsAsync(response.Collections); + await SyncCiphersAsync(userId, response.Ciphers); + await SyncSettingsAsync(userId, response.Domains); + await SyncPoliciesAsync(userId, response.Policies); + await SyncSendsAsync(userId, response.Sends); + await SetLastSyncAsync(now); + _watchDeviceService.Value.SyncDataToWatchAsync().FireAndForget(); + + return SyncCompleted(true); + } + catch + { + if (allowThrowOnError) + { + throw; + } + else + { + return SyncCompleted(false); + } + } + } + + public async Task SyncUpsertFolderAsync(SyncFolderNotification notification, bool isEdit) + { + SyncStarted(); + if (await _stateService.IsAuthenticatedAsync()) + { + try + { + var localFolder = await _folderService.GetAsync(notification.Id); + if ((!isEdit && localFolder == null) || + (isEdit && localFolder != null && localFolder.RevisionDate < notification.RevisionDate)) + { + var remoteFolder = await _apiService.GetFolderAsync(notification.Id); + if (remoteFolder != null) + { + var userId = await _stateService.GetActiveUserIdAsync(); + await _folderService.UpsertAsync(new FolderData(remoteFolder, userId)); + _messagingService.Send("syncedUpsertedFolder", new Dictionary + { + ["folderId"] = notification.Id + }); + return SyncCompleted(true); + } + } + } + catch { } + } + return SyncCompleted(false); + } + + public async Task SyncDeleteFolderAsync(SyncFolderNotification notification) + { + SyncStarted(); + if (await _stateService.IsAuthenticatedAsync()) + { + await _folderService.DeleteAsync(notification.Id); + _messagingService.Send("syncedDeletedFolder", new Dictionary + { + ["folderId"] = notification.Id + }); + return SyncCompleted(true); + } + return SyncCompleted(false); + } + + public async Task SyncUpsertCipherAsync(SyncCipherNotification notification, bool isEdit) + { + SyncStarted(); + if (await _stateService.IsAuthenticatedAsync()) + { + try + { + var shouldUpdate = true; + var localCipher = await _cipherService.GetAsync(notification.Id); + if (localCipher != null && localCipher.RevisionDate >= notification.RevisionDate) + { + shouldUpdate = false; + } + + var checkCollections = false; + if (shouldUpdate) + { + if (isEdit) + { + shouldUpdate = localCipher != null; + checkCollections = true; + } + else + { + if (notification.CollectionIds == null || notification.OrganizationId == null) + { + shouldUpdate = localCipher == null; + } + else + { + shouldUpdate = false; + checkCollections = true; + } + } + } + + if (!shouldUpdate && checkCollections && notification.OrganizationId != null && + notification.CollectionIds != null && notification.CollectionIds.Any()) + { + var collections = await _collectionService.GetAllAsync(); + if (collections != null) + { + foreach (var c in collections) + { + if (notification.CollectionIds.Contains(c.Id)) + { + shouldUpdate = true; + break; + } + } + } + } + + if (shouldUpdate) + { + var remoteCipher = await _apiService.GetCipherAsync(notification.Id); + if (remoteCipher != null) + { + var userId = await _stateService.GetActiveUserIdAsync(); + await _cipherService.UpsertAsync(new CipherData(remoteCipher, userId)); + _messagingService.Send("syncedUpsertedCipher", new Dictionary + { + ["cipherId"] = notification.Id + }); + return SyncCompleted(true); + } + } + } + catch (ApiException e) + { + if (e.Error != null && e.Error.StatusCode == System.Net.HttpStatusCode.NotFound && isEdit) + { + await _cipherService.DeleteAsync(notification.Id); + _messagingService.Send("syncedDeletedCipher", new Dictionary + { + ["cipherId"] = notification.Id + }); + return SyncCompleted(true); + } + } + } + return SyncCompleted(false); + } + + public async Task SyncDeleteCipherAsync(SyncCipherNotification notification) + { + SyncStarted(); + if (await _stateService.IsAuthenticatedAsync()) + { + await _cipherService.DeleteAsync(notification.Id); + _messagingService.Send("syncedDeletedCipher", new Dictionary + { + ["cipherId"] = notification.Id + }); + return SyncCompleted(true); + } + return SyncCompleted(false); + } + + // Helpers + + private void SyncStarted() + { + SyncInProgress = true; + _messagingService.Send("syncStarted"); + } + + private bool SyncCompleted(bool successfully) + { + SyncInProgress = false; + _messagingService.Send("syncCompleted", new Dictionary { ["successfully"] = successfully }); + return successfully; + } + + private async Task> NeedsSyncingAsync(bool forceSync) + { + if (forceSync) + { + return new Tuple(true, false); + } + var lastSync = await GetLastSyncAsync(); + if (lastSync == null || lastSync == DateTime.MinValue) + { + return new Tuple(true, false); + } + try + { + var response = await _apiService.GetAccountRevisionDateAsync(); + var d = CoreHelpers.Epoc.AddMilliseconds(response); + if (d <= lastSync.Value) + { + return new Tuple(false, false); + } + return new Tuple(true, false); + } + catch + { + return new Tuple(false, true); + } + } + + private async Task SyncProfileAsync(ProfileResponse response) + { + var stamp = await _stateService.GetSecurityStampAsync(); + if (stamp != null && stamp != response.SecurityStamp) + { + if (_logoutCallbackAsync != null) + { + await _logoutCallbackAsync(new Tuple(response.Id, false, true)); + } + return; + } + await _cryptoService.SetEncKeyAsync(response.Key); + await _cryptoService.SetEncPrivateKeyAsync(response.PrivateKey); + await _cryptoService.SetOrgKeysAsync(response.Organizations); + await _stateService.SetSecurityStampAsync(response.SecurityStamp); + var organizations = response.Organizations.ToDictionary(o => o.Id, o => new OrganizationData(o)); + await _organizationService.ReplaceAsync(organizations); + await _stateService.SetEmailVerifiedAsync(response.EmailVerified); + await _stateService.SetNameAsync(response.Name); + await _stateService.SetPersonalPremiumAsync(response.Premium); + await _stateService.SetAvatarColorAsync(response.AvatarColor); + await _keyConnectorService.SetUsesKeyConnector(response.UsesKeyConnector); + } + + private async Task SyncFoldersAsync(string userId, List response) + { + var folders = response.ToDictionary(f => f.Id, f => new FolderData(f, userId)); + await _folderService.ReplaceAsync(folders); + } + + private async Task SyncCollectionsAsync(List response) + { + var collections = response.ToDictionary(c => c.Id, c => new CollectionData(c)); + await _collectionService.ReplaceAsync(collections); + } + + private async Task SyncCiphersAsync(string userId, List response) + { + var ciphers = response.ToDictionary(c => c.Id, c => new CipherData(c, userId)); + await _cipherService.ReplaceAsync(ciphers); + } + + private async Task SyncSettingsAsync(string userId, DomainsResponse response) + { + var eqDomains = new List>(); + if (response != null && response.EquivalentDomains != null) + { + eqDomains = eqDomains.Concat(response.EquivalentDomains).ToList(); + } + if (response != null && response.GlobalEquivalentDomains != null) + { + foreach (var global in response.GlobalEquivalentDomains) + { + if (global.Domains.Any()) + { + eqDomains.Add(global.Domains); + } + } + } + await _settingsService.SetEquivalentDomainsAsync(eqDomains); + } + + private async Task SyncPoliciesAsync(string userId, List response) + { + var policies = response?.ToDictionary(p => p.Id, p => new PolicyData(p)) ?? + new Dictionary(); + await _policyService.Replace(policies, userId); + } + + private async Task SyncSendsAsync(string userId, List response) + { + var sends = response?.ToDictionary(s => s.Id, s => new SendData(s, userId)) ?? + new Dictionary(); + await _sendService.ReplaceAsync(sends); + } + + public async Task SyncPasswordlessLoginRequestsAsync() + { + try + { + var userId = await _stateService.GetActiveUserIdAsync(); + // if the user has not enabled passwordless logins ignore requests + if (!await _stateService.GetApprovePasswordlessLoginsAsync(userId)) + { + return; + } + + var loginRequests = await _apiService.GetAuthRequestAsync(); + if (loginRequests == null || !loginRequests.Any()) + { + return; + } + + var validLoginRequest = loginRequests.Where(l => !l.IsAnswered && !l.IsExpired) + .OrderByDescending(x => x.CreationDate) + .FirstOrDefault(); + + if (validLoginRequest is null) + { + return; + } + + await _stateService.SetPasswordlessLoginNotificationAsync(new PasswordlessRequestNotification() + { + Id = validLoginRequest.Id, + UserId = userId + }); + + _messagingService.Send(Constants.PasswordlessLoginRequestKey); + } + catch (Exception ex) + { + _logger.Exception(ex); + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/TokenService.cs b/src/Maui/Bitwarden/Core/Services/TokenService.cs new file mode 100644 index 000000000..30bbbe565 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/TokenService.cs @@ -0,0 +1,236 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Utilities; +using Newtonsoft.Json.Linq; + +namespace Bit.Core.Services +{ + public class TokenService : ITokenService + { + private readonly IStateService _stateService; + + private string _accessTokenForDecoding; + private JObject _decodedAccessToken; + + public TokenService(IStateService stateService) + { + _stateService = stateService; + } + + public async Task SetTokensAsync(string accessToken, string refreshToken) + { + await Task.WhenAll( + SetAccessTokenAsync(accessToken), + SetRefreshTokenAsync(refreshToken)); + } + + public async Task SetAccessTokenAsync(string accessToken, bool forDecodeOnly = false) + { + _accessTokenForDecoding = accessToken; + _decodedAccessToken = null; + if (!forDecodeOnly) + { + await _stateService.SetAccessTokenAsync(accessToken, await SkipTokenStorage()); + } + } + + public async Task GetTokenAsync() + { + _accessTokenForDecoding = await _stateService.GetAccessTokenAsync(); + return _accessTokenForDecoding; + } + + public async Task PrepareTokenForDecodingAsync() + { + _accessTokenForDecoding = await _stateService.GetAccessTokenAsync(); + } + + public async Task SetRefreshTokenAsync(string refreshToken) + { + await _stateService.SetRefreshTokenAsync(refreshToken, await SkipTokenStorage()); + } + + public async Task GetRefreshTokenAsync() + { + return await _stateService.GetRefreshTokenAsync(); + } + + public async Task ToggleTokensAsync() + { + // load and re-save tokens to reflect latest value of SkipTokenStorage() + var token = await GetTokenAsync(); + var refreshToken = await GetRefreshTokenAsync(); + await SetAccessTokenAsync(token); + await SetRefreshTokenAsync(refreshToken); + } + + public async Task SetTwoFactorTokenAsync(string token, string email) + { + await _stateService.SetTwoFactorTokenAsync(token, email); + } + + public async Task GetTwoFactorTokenAsync(string email) + { + return await _stateService.GetTwoFactorTokenAsync(email); + } + + public async Task ClearTwoFactorTokenAsync(string email) + { + await _stateService.SetTwoFactorTokenAsync(null, email); + } + + public async Task ClearTokenAsync(string userId = null) + { + ClearCache(); + await Task.WhenAll( + _stateService.SetAccessTokenAsync(null, false, userId), + _stateService.SetRefreshTokenAsync(null, false, userId)); + } + + public void ClearCache() + { + _accessTokenForDecoding = null; + _decodedAccessToken = null; + } + + public JObject DecodeToken() + { + if (_decodedAccessToken != null) + { + return _decodedAccessToken; + } + if (_accessTokenForDecoding == null) + { + throw new InvalidOperationException("Access token not found."); + } + var parts = _accessTokenForDecoding.Split('.'); + if (parts.Length != 3) + { + throw new InvalidOperationException("JWT must have 3 parts."); + } + var decodedBytes = CoreHelpers.Base64UrlDecode(parts[1]); + if (decodedBytes == null || decodedBytes.Length < 1) + { + throw new InvalidOperationException("Cannot decode the token."); + } + _decodedAccessToken = JObject.Parse(Encoding.UTF8.GetString(decodedBytes)); + return _decodedAccessToken; + } + + public DateTime? GetTokenExpirationDate() + { + var decoded = DecodeToken(); + if (decoded?["exp"] == null) + { + return null; + } + return CoreHelpers.Epoc.AddSeconds(Convert.ToDouble(decoded["exp"].Value())); + } + + public int TokenSecondsRemaining() + { + var d = GetTokenExpirationDate(); + if (d == null) + { + return 0; + } + var timeRemaining = d.Value - DateTime.UtcNow; + return (int)timeRemaining.TotalSeconds; + } + + public bool TokenNeedsRefresh(int minutes = 5) + { + var sRemaining = TokenSecondsRemaining(); + return sRemaining < (60 * minutes); + } + + public string GetUserId() + { + var decoded = DecodeToken(); + if (decoded?["sub"] == null) + { + throw new Exception("No user id found."); + } + return decoded["sub"].Value(); + } + + public string GetEmail() + { + var decoded = DecodeToken(); + if (decoded?["email"] == null) + { + throw new Exception("No email found."); + } + return decoded["email"].Value(); + } + + public bool GetEmailVerified() + { + var decoded = DecodeToken(); + if (decoded?["email_verified"] == null) + { + throw new Exception("No email verification found."); + } + return decoded["email_verified"].Value(); + } + + public string GetName() + { + var decoded = DecodeToken(); + if (decoded?["name"] == null) + { + return null; + } + return decoded["name"].Value(); + } + + public bool GetPremium() + { + var decoded = DecodeToken(); + if (decoded?["premium"] == null) + { + return false; + } + return decoded["premium"].Value(); + } + + public string GetIssuer() + { + var decoded = DecodeToken(); + if (decoded?["iss"] == null) + { + throw new Exception("No issuer found."); + } + return decoded["iss"].Value(); + } + + public async Task GetIsExternal() + { + if (_accessTokenForDecoding == null) + { + await GetTokenAsync(); + if (_accessTokenForDecoding == null) + { + return false; + } + } + var decoded = DecodeToken(); + if (decoded?["amr"] == null) + { + return false; + } + return decoded["amr"].Value().Any(t => t.Value() == "external"); + } + + private async Task SkipTokenStorage() + { + var timeout = await _stateService.GetVaultTimeoutAsync(); + var action = await _stateService.GetVaultTimeoutActionAsync(); + return timeout.HasValue && action == VaultTimeoutAction.Logout; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/TotpService.cs b/src/Maui/Bitwarden/Core/Services/TotpService.cs new file mode 100644 index 000000000..4f1e453ec --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/TotpService.cs @@ -0,0 +1,116 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Utilities; + +namespace Bit.Core.Services +{ + public class TotpService : ITotpService + { + private const string SteamChars = "23456789BCDFGHJKMNPQRTVWXY"; + + private readonly ICryptoFunctionService _cryptoFunctionService; + + public TotpService( + ICryptoFunctionService cryptoFunctionService) + { + _cryptoFunctionService = cryptoFunctionService; + } + + public async Task GetCodeAsync(string key) + { + if (string.IsNullOrWhiteSpace(key)) + { + return null; + } + var period = Constants.TotpDefaultTimer; + var alg = CryptoHashAlgorithm.Sha1; + var digits = 6; + var keyB32 = key; + + var isOtpAuth = key?.ToLowerInvariant().StartsWith("otpauth://") ?? false; + var isSteamAuth = key?.ToLowerInvariant().StartsWith("steam://") ?? false; + if (isOtpAuth) + { + var otpData = new OtpData(key.ToLowerInvariant()); + if (otpData.Digits > 0) + { + digits = Math.Min(otpData.Digits.Value, 10); + } + if (otpData.Period.HasValue) + { + period = otpData.Period.Value; + } + if (otpData.Secret != null) + { + keyB32 = otpData.Secret; + } + if (otpData.Algorithm.HasValue) + { + alg = otpData.Algorithm.Value; + } + } + else if (isSteamAuth) + { + digits = 5; + keyB32 = key.Substring(8); + } + + var keyBytes = Base32.FromBase32(keyB32); + if (keyBytes == null || keyBytes.Length == 0) + { + return null; + } + var now = CoreHelpers.EpocUtcNow() / 1000; + var time = now / period; + var timeBytes = BitConverter.GetBytes(time); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(timeBytes, 0, timeBytes.Length); + } + + var hash = await _cryptoFunctionService.HmacAsync(timeBytes, keyBytes, alg); + if (hash.Length == 0) + { + return null; + } + + var offset = (hash[hash.Length - 1] & 0xf); + var binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | + ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff); + + string otp = string.Empty; + if (isSteamAuth) + { + var fullCode = binary & 0x7fffffff; + for (var i = 0; i < digits; i++) + { + otp += SteamChars[fullCode % SteamChars.Length]; + fullCode = (int)Math.Truncate(fullCode / (double)SteamChars.Length); + } + } + else + { + var rawOtp = binary % (int)Math.Pow(10, digits); + otp = rawOtp.ToString().PadLeft(digits, '0'); + } + return otp; + } + + public int GetTimeInterval(string key) + { + var period = Constants.TotpDefaultTimer; + if (key != null && key.ToLowerInvariant().StartsWith("otpauth://")) + { + var qsParams = CoreHelpers.GetQueryParams(key); + if (qsParams.ContainsKey("period") && qsParams["period"] != null && + int.TryParse(qsParams["period"].Trim(), out var periodParam) && periodParam > 0) + { + period = periodParam; + } + } + return period; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/UserVerificationService.cs b/src/Maui/Bitwarden/Core/Services/UserVerificationService.cs new file mode 100644 index 000000000..74031b55c --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/UserVerificationService.cs @@ -0,0 +1,67 @@ +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Request; + +namespace Bit.Core.Services +{ + public class UserVerificationService : IUserVerificationService + { + private readonly IApiService _apiService; + private readonly IPlatformUtilsService _platformUtilsService; + private readonly II18nService _i18nService; + private readonly ICryptoService _cryptoService; + + public UserVerificationService(IApiService apiService, IPlatformUtilsService platformUtilsService, + II18nService i18nService, ICryptoService cryptoService) + { + _apiService = apiService; + _platformUtilsService = platformUtilsService; + _i18nService = i18nService; + _cryptoService = cryptoService; + } + + async public Task VerifyUser(string secret, VerificationType verificationType) + { + if (string.IsNullOrEmpty(secret)) + { + await InvalidSecretErrorAsync(verificationType); + return false; + } + + if (verificationType == VerificationType.OTP) + { + var request = new VerifyOTPRequest(secret); + try + { + await _apiService.PostAccountVerifyOTPAsync(request); + } + catch + { + await InvalidSecretErrorAsync(verificationType); + return false; + } + } + else + { + var passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(secret, null); + if (!passwordValid) + { + await InvalidSecretErrorAsync(verificationType); + return false; + } + } + + return true; + } + + async private Task InvalidSecretErrorAsync(VerificationType verificationType) + { + var errorMessage = verificationType == VerificationType.OTP + ? _i18nService.T("InvalidVerificationCode") + : _i18nService.T("InvalidMasterPassword"); + + await _platformUtilsService.ShowDialogAsync(errorMessage); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/UsernameGenerationService.cs b/src/Maui/Bitwarden/Core/Services/UsernameGenerationService.cs new file mode 100644 index 000000000..9ec9fa215 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/UsernameGenerationService.cs @@ -0,0 +1,185 @@ +using System.Text; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Services.EmailForwarders; +using Bit.Core.Utilities; + +namespace Bit.Core.Services +{ + public class UsernameGenerationService : IUsernameGenerationService + { + private const string CATCH_ALL_EMAIL_DOMAIN_FORMAT = "{0}@{1}"; + private readonly ICryptoService _cryptoService; + private readonly IApiService _apiService; + private readonly IStateService _stateService; + readonly LazyResolve _logger = new LazyResolve(); + private UsernameGenerationOptions _optionsCache; + + public UsernameGenerationService( + ICryptoService cryptoService, + IApiService apiService, + IStateService stateService) + { + _cryptoService = cryptoService; + _apiService = apiService; + _stateService = stateService; + } + + public async Task GenerateAsync(UsernameGenerationOptions options) + { + switch (options.Type) + { + case UsernameType.PlusAddressedEmail: + return await GeneratePlusAddressedEmailAsync(options); + case UsernameType.CatchAllEmail: + return await GenerateCatchAllAsync(options); + case UsernameType.ForwardedEmailAlias: + return await GenerateForwardedEmailAliasAsync(options); + case UsernameType.RandomWord: + return await GenerateRandomWordAsync(options); + default: + _logger.Value.Error($"Error UsernameGenerationService: UsernameType {options.Type} not implemented."); + return string.Empty; + } + } + + public async Task GetOptionsAsync() + { + if (_optionsCache == null) + { + var options = await _stateService.GetUsernameGenerationOptionsAsync(); + _optionsCache = options ?? new UsernameGenerationOptions(); + } + + return _optionsCache; + } + public async Task SaveOptionsAsync(UsernameGenerationOptions options) + { + await _stateService.SetUsernameGenerationOptionsAsync(options); + _optionsCache = options; + } + + public void ClearCache() + { + _optionsCache = null; + } + + private async Task GenerateRandomWordAsync(UsernameGenerationOptions options) + { + var listLength = EEFLongWordList.Instance.List.Count - 1; + var wordIndex = await _cryptoService.RandomNumberAsync(0, listLength); + var randomWord = EEFLongWordList.Instance.List[wordIndex]; + + if (string.IsNullOrWhiteSpace(randomWord)) + { + _logger.Value.Error($"Error UsernameGenerationService: EEFLongWordList has NullOrWhiteSpace value at {wordIndex} index."); + return Constants.DefaultUsernameGenerated; + } + + if (options.CapitalizeRandomWordUsername) + { + randomWord = Capitalize(randomWord); + } + + if (options.IncludeNumberRandomWordUsername) + { + randomWord = await AppendRandomNumberToRandomWordAsync(randomWord); + } + + return randomWord; + } + + private async Task GeneratePlusAddressedEmailAsync(UsernameGenerationOptions options) + { + if (string.IsNullOrWhiteSpace(options.PlusAddressedEmail) || options.PlusAddressedEmail.Length < 3) + { + return Constants.DefaultUsernameGenerated; + } + + var atIndex = options.PlusAddressedEmail.IndexOf("@"); + if (atIndex < 1 || atIndex >= options.PlusAddressedEmail.Length - 1) + { + return options.PlusAddressedEmail; + } + + if (options.PlusAddressedEmailType == UsernameEmailType.Random) + { + var randomString = await _cryptoService.RandomStringAsync(8); + return options.PlusAddressedEmail.Insert(atIndex, $"+{randomString}"); + } + else + { + return options.PlusAddressedEmail.Insert(atIndex, $"+{options.EmailWebsite}"); + } + } + + private async Task GenerateCatchAllAsync(UsernameGenerationOptions options) + { + var catchAllEmailDomain = options.CatchAllEmailDomain; + + if (string.IsNullOrWhiteSpace(catchAllEmailDomain)) + { + return Constants.DefaultUsernameGenerated; + } + + if (options.CatchAllEmailType == UsernameEmailType.Random) + { + var randomString = await _cryptoService.RandomStringAsync(8); + return string.Format(CATCH_ALL_EMAIL_DOMAIN_FORMAT, randomString, catchAllEmailDomain); + } + + return string.Format(CATCH_ALL_EMAIL_DOMAIN_FORMAT, options.EmailWebsite, catchAllEmailDomain); + } + + private async Task GenerateForwardedEmailAliasAsync(UsernameGenerationOptions options) + { + if (options.ServiceType == ForwardedEmailServiceType.AnonAddy) + { + return await new AnonAddyForwarder() + .GenerateAsync(_apiService, (AnonAddyForwarderOptions)options.GetForwarderOptions()); + } + + BaseForwarder simpleForwarder = null; + + switch (options.ServiceType) + { + case ForwardedEmailServiceType.FirefoxRelay: + simpleForwarder = new FirefoxRelayForwarder(); + break; + case ForwardedEmailServiceType.SimpleLogin: + simpleForwarder = new SimpleLoginForwarder(); + break; + case ForwardedEmailServiceType.DuckDuckGo: + simpleForwarder = new DuckDuckGoForwarder(); + break; + case ForwardedEmailServiceType.Fastmail: + simpleForwarder = new FastmailForwarder(); + break; + default: + _logger.Value.Error($"Error UsernameGenerationService: ForwardedEmailServiceType {options.ServiceType} not implemented."); + return Constants.DefaultUsernameGenerated; + } + + return await simpleForwarder.GenerateAsync(_apiService, options.GetForwarderOptions()); + } + + private string Capitalize(string str) + { + return char.ToUpper(str[0]) + str.Substring(1); + } + + private async Task AppendRandomNumberToRandomWordAsync(string word) + { + if (string.IsNullOrWhiteSpace(word)) + { + return word; + } + + var randomNumber = await _cryptoService.RandomNumberAsync(1, 9999); + + return word + randomNumber.ToString("0000"); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Services/VaultTimeoutService.cs b/src/Maui/Bitwarden/Core/Services/VaultTimeoutService.cs new file mode 100644 index 000000000..f5bd7d4bd --- /dev/null +++ b/src/Maui/Bitwarden/Core/Services/VaultTimeoutService.cs @@ -0,0 +1,250 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Services +{ + public class VaultTimeoutService : IVaultTimeoutService + { + private readonly ICryptoService _cryptoService; + private readonly IStateService _stateService; + private readonly IPlatformUtilsService _platformUtilsService; + private readonly IFolderService _folderService; + private readonly ICipherService _cipherService; + private readonly ICollectionService _collectionService; + private readonly ISearchService _searchService; + private readonly IMessagingService _messagingService; + private readonly ITokenService _tokenService; + private readonly IKeyConnectorService _keyConnectorService; + private readonly Func, Task> _lockedCallback; + private readonly Func, Task> _loggedOutCallback; + + public VaultTimeoutService( + ICryptoService cryptoService, + IStateService stateService, + IPlatformUtilsService platformUtilsService, + IFolderService folderService, + ICipherService cipherService, + ICollectionService collectionService, + ISearchService searchService, + IMessagingService messagingService, + ITokenService tokenService, + IKeyConnectorService keyConnectorService, + Func, Task> lockedCallback, + Func, Task> loggedOutCallback) + { + _cryptoService = cryptoService; + _stateService = stateService; + _platformUtilsService = platformUtilsService; + _folderService = folderService; + _cipherService = cipherService; + _collectionService = collectionService; + _searchService = searchService; + _messagingService = messagingService; + _tokenService = tokenService; + _keyConnectorService = keyConnectorService; + _lockedCallback = lockedCallback; + _loggedOutCallback = loggedOutCallback; + } + + public long? DelayLockAndLogoutMs { get; set; } + + public async Task IsLockedAsync(string userId = null) + { + var hasKey = await _cryptoService.HasKeyAsync(userId); + if (hasKey) + { + var biometricSet = await IsBiometricLockSetAsync(userId); + if (biometricSet && await _stateService.GetBiometricLockedAsync(userId)) + { + return true; + } + } + return !hasKey; + } + + public async Task ShouldLockAsync(string userId = null) + { + return await ShouldTimeoutAsync(userId) + && + await _stateService.GetVaultTimeoutActionAsync(userId) == VaultTimeoutAction.Lock; + } + + public async Task IsLoggedOutByTimeoutAsync(string userId = null) + { + var authed = await _stateService.IsAuthenticatedAsync(userId); + var email = await _stateService.GetEmailAsync(userId); + return !authed && !string.IsNullOrWhiteSpace(email); + } + + public async Task ShouldLogOutByTimeoutAsync(string userId = null) + { + return await ShouldTimeoutAsync(userId) + && + await _stateService.GetVaultTimeoutActionAsync(userId) == VaultTimeoutAction.Logout; + } + + public async Task CheckVaultTimeoutAsync() + { + if (_platformUtilsService.IsViewOpen()) + { + return; + } + + if (await ShouldTimeoutAsync()) + { + await ExecuteTimeoutActionAsync(); + } + } + + public async Task ShouldTimeoutAsync(string userId = null) + { + var authed = await _stateService.IsAuthenticatedAsync(userId); + if (!authed) + { + return false; + } + var vaultTimeoutAction = await _stateService.GetVaultTimeoutActionAsync(userId); + if (vaultTimeoutAction == VaultTimeoutAction.Lock && await IsLockedAsync(userId)) + { + return false; + } + var vaultTimeoutMinutes = await GetVaultTimeout(userId); + if (vaultTimeoutMinutes < 0 || vaultTimeoutMinutes == null) + { + return false; + } + if (vaultTimeoutMinutes == 0 && !DelayLockAndLogoutMs.HasValue) + { + return true; + } + var lastActiveTime = await _stateService.GetLastActiveTimeAsync(userId); + if (lastActiveTime == null) + { + return false; + } + var diffMs = _platformUtilsService.GetActiveTime() - lastActiveTime; + if (DelayLockAndLogoutMs.HasValue && diffMs < DelayLockAndLogoutMs) + { + return false; + } + var vaultTimeoutMs = vaultTimeoutMinutes * 60000; + return diffMs >= vaultTimeoutMs; + } + + public async Task ExecuteTimeoutActionAsync(string userId = null) + { + var action = await _stateService.GetVaultTimeoutActionAsync(userId); + if (action == VaultTimeoutAction.Logout) + { + await LogOutAsync(false, userId); + } + else + { + await LockAsync(true, false, userId); + } + } + + public async Task LockAsync(bool allowSoftLock = false, bool userInitiated = false, string userId = null) + { + var authed = await _stateService.IsAuthenticatedAsync(userId); + if (!authed) + { + return; + } + + var isActiveAccount = await _stateService.IsActiveAccountAsync(userId); + + if (userId == null) + { + userId = await _stateService.GetActiveUserIdAsync(); + } + + if (await _keyConnectorService.GetUsesKeyConnector()) + { + var (isPinProtected, isPinProtectedWithKey) = await IsPinLockSetAsync(userId); + var pinLock = (isPinProtected && await _stateService.GetPinProtectedKeyAsync(userId) != null) || + isPinProtectedWithKey; + + if (!pinLock && !await IsBiometricLockSetAsync()) + { + await LogOutAsync(userInitiated, userId); + return; + } + } + + if (allowSoftLock) + { + var isBiometricLockSet = await IsBiometricLockSetAsync(userId); + await _stateService.SetBiometricLockedAsync(isBiometricLockSet, userId); + if (isBiometricLockSet) + { + _lockedCallback?.Invoke(new Tuple(userId, userInitiated)); + return; + } + } + await Task.WhenAll( + _cryptoService.ClearKeyAsync(userId), + _cryptoService.ClearOrgKeysAsync(true, userId), + _cryptoService.ClearKeyPairAsync(true, userId), + _cryptoService.ClearEncKeyAsync(true, userId)); + + if (isActiveAccount) + { + _folderService.ClearCache(); + await _cipherService.ClearCacheAsync(); + _collectionService.ClearCache(); + _searchService.ClearIndex(); + } + _lockedCallback?.Invoke(new Tuple(userId, userInitiated)); + } + + public async Task LogOutAsync(bool userInitiated = true, string userId = null) + { + if (_loggedOutCallback != null) + { + await _loggedOutCallback.Invoke(new Tuple(userId, userInitiated, false)); + } + } + + public async Task SetVaultTimeoutOptionsAsync(int? timeout, VaultTimeoutAction? action) + { + await _stateService.SetVaultTimeoutAsync(timeout); + await _stateService.SetVaultTimeoutActionAsync(action); + await _cryptoService.ToggleKeyAsync(); + await _tokenService.ToggleTokensAsync(); + } + + public async Task> IsPinLockSetAsync(string userId = null) + { + var protectedPin = await _stateService.GetProtectedPinAsync(userId); + var pinProtectedKey = await _stateService.GetPinProtectedAsync(userId); + return new Tuple(protectedPin != null, pinProtectedKey != null); + } + + public async Task IsBiometricLockSetAsync(string userId = null) + { + var biometricLock = await _stateService.GetBiometricUnlockAsync(userId); + return biometricLock.GetValueOrDefault(); + } + + public async Task ClearAsync(string userId = null) + { + await _stateService.SetPinProtectedKeyAsync(null, userId); + await _stateService.SetProtectedPinAsync(null, userId); + } + + public async Task GetVaultTimeout(string userId = null) + { + return await _stateService.GetVaultTimeoutAsync(userId); + } + + public async Task GetVaultTimeoutAction(string userId = null) + { + return await _stateService.GetVaultTimeoutActionAsync(userId); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Utilities/AccountsManagerMessageCommands.cs b/src/Maui/Bitwarden/Core/Utilities/AccountsManagerMessageCommands.cs new file mode 100644 index 000000000..c5af40e78 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Utilities/AccountsManagerMessageCommands.cs @@ -0,0 +1,14 @@ +namespace Bit.Core.Utilities +{ + public static class AccountsManagerMessageCommands + { + public const string LOCKED = "locked"; + public const string LOCK_VAULT = "lockVault"; + public const string LOGOUT = "logout"; + public const string LOGGED_OUT = "loggedOut"; + public const string ADD_ACCOUNT = "addAccount"; + public const string ACCOUNT_ADDED = "accountAdded"; + public const string SWITCHED_ACCOUNT = "switchedAccount"; + public const string ACCOUNT_SWITCH_COMPLETED = "accountSwitchCompleted"; + } +} diff --git a/src/Maui/Bitwarden/Core/Utilities/Base32.cs b/src/Maui/Bitwarden/Core/Utilities/Base32.cs new file mode 100644 index 000000000..04c8bf5cd --- /dev/null +++ b/src/Maui/Bitwarden/Core/Utilities/Base32.cs @@ -0,0 +1,72 @@ +using System; + +namespace Bit.Core.Utilities +{ + // ref: https://github.com/aspnet/Identity/blob/dev/src/Microsoft.Extensions.Identity.Core/Base32.cs + // with some modifications for cleaning input + public static class Base32 + { + private static readonly string _base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + + public static byte[] FromBase32(string input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + input = input.ToUpperInvariant(); + var cleanedInput = string.Empty; + foreach (var c in input) + { + if (_base32Chars.IndexOf(c) < 0) + { + continue; + } + + cleanedInput += c; + } + + input = cleanedInput; + if (input.Length == 0) + { + return new byte[0]; + } + + var output = new byte[input.Length * 5 / 8]; + var bitIndex = 0; + var inputIndex = 0; + var outputBits = 0; + var outputIndex = 0; + + while (outputIndex < output.Length) + { + var byteIndex = _base32Chars.IndexOf(input[inputIndex]); + if (byteIndex < 0) + { + throw new FormatException(); + } + + var bits = Math.Min(5 - bitIndex, 8 - outputBits); + output[outputIndex] <<= bits; + output[outputIndex] |= (byte)(byteIndex >> (5 - (bitIndex + bits))); + + bitIndex += bits; + if (bitIndex >= 5) + { + inputIndex++; + bitIndex = 0; + } + + outputBits += bits; + if (outputBits >= 8) + { + outputIndex++; + outputBits = 0; + } + } + + return output; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Utilities/CipherTypeExtensions.cs b/src/Maui/Bitwarden/Core/Utilities/CipherTypeExtensions.cs new file mode 100644 index 000000000..b306fc4ce --- /dev/null +++ b/src/Maui/Bitwarden/Core/Utilities/CipherTypeExtensions.cs @@ -0,0 +1,14 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Utilities +{ + public static class CipherTypeExtensions + { + public static bool IsEqualToOrCanSignIn(this CipherType type, CipherType type2) + { + return type == type2 + || + (type == CipherType.Login && type2 == CipherType.Fido2Key); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Utilities/CoreHelpers.cs b/src/Maui/Bitwarden/Core/Utilities/CoreHelpers.cs new file mode 100644 index 000000000..2fa4f4ad0 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Utilities/CoreHelpers.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text.RegularExpressions; +using System.Web; +using Bit.Core.Models.Domain; +using Bit.Core.Services; +using Newtonsoft.Json; + +namespace Bit.Core.Utilities +{ + public static class CoreHelpers + { + public static readonly string IpRegex = + "^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." + + "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." + + "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." + + "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"; + + public static readonly string TldEndingRegex = + ".*\\.(com|net|org|edu|uk|gov|ca|de|jp|fr|au|ru|ch|io|es|us|co|xyz|info|ly|mil)$"; + + public static readonly DateTime Epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + public static long EpocUtcNow() + { + return (long)(DateTime.UtcNow - Epoc).TotalMilliseconds; + } + + public static bool InDebugMode() + { +#if DEBUG + return true; +#else + return false; +#endif + } + + public static string GetHostname(string uriString) + { + var uri = GetUri(uriString); + return string.IsNullOrEmpty(uri?.Host) ? null : uri.Host; + } + + public static string GetHost(string uriString) + { + var uri = GetUri(uriString); + if (!string.IsNullOrEmpty(uri?.Host)) + { + if (uri.IsDefaultPort) + { + return uri.Host; + } + else + { + return string.Format("{0}:{1}", uri.Host, uri.Port); + } + } + return null; + } + + public static string GetDomain(string uriString) + { + var uri = GetUri(uriString); + if (uri == null) + { + return null; + } + + if (uri.Host == "localhost" || Regex.IsMatch(uri.Host, IpRegex)) + { + return uri.Host; + } + try + { + if (DomainName.TryParseBaseDomain(uri.Host, out var baseDomain)) + { + return baseDomain ?? uri.Host; + } + } + catch { } + return null; + } + + public static Uri GetUri(string uriString) + { + if (string.IsNullOrWhiteSpace(uriString)) + { + return null; + } + var hasHttpProtocol = uriString.StartsWith("http://") || uriString.StartsWith("https://"); + if (!hasHttpProtocol && !uriString.Contains("://") && uriString.Contains(".")) + { + if (Uri.TryCreate("http://" + uriString, UriKind.Absolute, out var uri)) + { + return uri; + } + } + if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri2)) + { + return uri2; + } + return null; + } + + public static void NestedTraverse(List> nodeTree, int partIndex, string[] parts, + T obj, T parent, char delimiter) where T : ITreeNodeObject + { + if (parts.Length <= partIndex) + { + return; + } + + var end = partIndex == parts.Length - 1; + var partName = parts[partIndex]; + foreach (var n in nodeTree) + { + if (n.Node.Name != parts[partIndex]) + { + continue; + } + if (end && n.Node.Id != obj.Id) + { + // Another node with the same name. + nodeTree.Add(new TreeNode(obj, partName, parent)); + return; + } + NestedTraverse(n.Children, partIndex + 1, parts, obj, n.Node, delimiter); + return; + } + if (!nodeTree.Any(n => n.Node.Name == partName)) + { + if (end) + { + nodeTree.Add(new TreeNode(obj, partName, parent)); + return; + } + var newPartName = string.Concat(parts[partIndex], delimiter, parts[partIndex + 1]); + var newParts = new List { newPartName }; + var newPartsStartFrom = partIndex + 2; + newParts.AddRange(new ArraySegment(parts, newPartsStartFrom, parts.Length - newPartsStartFrom)); + NestedTraverse(nodeTree, 0, newParts.ToArray(), obj, parent, delimiter); + } + } + + public static TreeNode GetTreeNodeObject(List> nodeTree, string id) where T : ITreeNodeObject + { + foreach (var n in nodeTree) + { + if (n.Node.Id == id) + { + return n; + } + else if (n.Children != null) + { + var node = GetTreeNodeObject(n.Children, id); + if (node != null) + { + return node; + } + } + } + return null; + } + + public static Dictionary GetQueryParams(string urlString) + { + try + { + if (Uri.TryCreate(urlString, UriKind.Absolute, out var uri)) + { + return GetQueryParams(uri); + } + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } + return new Dictionary(); + } + + public static Dictionary GetQueryParams(Uri uri) + { + try + { + if (string.IsNullOrWhiteSpace(uri.Query)) + { + return new Dictionary(); + } + + var queryStringNameValueCollection = HttpUtility.ParseQueryString(uri.Query); + return queryStringNameValueCollection.AllKeys.Where(k => k != null).ToDictionary(k => k, k => queryStringNameValueCollection[k]); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } + return new Dictionary(); + } + + public static string SerializeJson(object obj, bool ignoreNulls = false) + { + var jsonSerializationSettings = new JsonSerializerSettings(); + if (ignoreNulls) + { + jsonSerializationSettings.NullValueHandling = NullValueHandling.Ignore; + } + return JsonConvert.SerializeObject(obj, jsonSerializationSettings); + } + + public static string SerializeJson(object obj, JsonSerializerSettings jsonSerializationSettings) + { + return JsonConvert.SerializeObject(obj, jsonSerializationSettings); + } + + public static T DeserializeJson(string json, bool ignoreNulls = false) + { + var jsonSerializationSettings = new JsonSerializerSettings(); + if (ignoreNulls) + { + jsonSerializationSettings.NullValueHandling = NullValueHandling.Ignore; + } + return JsonConvert.DeserializeObject(json, jsonSerializationSettings); + } + + public static string Base64UrlEncode(byte[] input) + { + var output = Convert.ToBase64String(input) + .Replace('+', '-') + .Replace('/', '_') + .Replace("=", string.Empty); + return output; + } + + public static byte[] Base64UrlDecode(string input) + { + var output = input; + // 62nd char of encoding + output = output.Replace('-', '+'); + // 63rd char of encoding + output = output.Replace('_', '/'); + // Pad with trailing '='s + switch (output.Length % 4) + { + case 0: + // No pad chars in this case + break; + case 2: + // Two pad chars + output += "=="; break; + case 3: + // One pad char + output += "="; break; + default: + throw new InvalidOperationException("Illegal base64url string!"); + } + // Standard base64 decoder + return Convert.FromBase64String(output); + } + + public static T Clone(T obj) + { + return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj)); + } + + public static string TextColorFromBgColor(string hexColor, int threshold = 166) + { + if (new ColorConverter().ConvertFromString(hexColor) is Color bgColor) + { + var luminance = bgColor.R * 0.299 + bgColor.G * 0.587 + bgColor.B * 0.114; + return luminance > threshold ? "#ff000000" : "#ffffffff"; + } + + return "#ff000000"; + } + + public static string StringToColor(string str, string fallback) + { + if (str == null) + { + return fallback; + } + var hash = 0; + for (var i = 0; i < str.Length; i++) + { + hash = str[i] + ((hash << 5) - hash); + } + var color = "#FF"; + for (var i = 0; i < 3; i++) + { + var value = (hash >> (i * 8)) & 0xff; + color += Convert.ToString(value, 16).PadLeft(2, '0'); + } + return color; + } + } +} diff --git a/src/Maui/Bitwarden/Core/Utilities/DomainName.cs b/src/Maui/Bitwarden/Core/Utilities/DomainName.cs new file mode 100644 index 000000000..bbc155f04 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Utilities/DomainName.cs @@ -0,0 +1,340 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Bit.Core.Utilities +{ + // ref: https://github.com/danesparza/domainname-parser + public class DomainName + { + private readonly string _subDomain = string.Empty; + private readonly string _domain = string.Empty; + private readonly string _tld = string.Empty; + private readonly TLDRule _tldRule = null; + + public string SubDomain => _subDomain; + public string Domain => _domain; + public string Sld => _domain; + public string Tld => _tld; + public TLDRule Rule => _tldRule; + public string BaseDomain => $"{_domain}.{_tld}"; + + public DomainName(string tld, string sld, string subDdomain, TLDRule tldRule) + { + _tld = tld; + _domain = sld; + _subDomain = subDdomain; + _tldRule = tldRule; + } + + public static bool TryParse(string domainString, out DomainName result) + { + bool retval = false; + // Our temporary domain parts: + var tld = string.Empty; + var sld = string.Empty; + var subdomain = string.Empty; + TLDRule _tldrule = null; + result = null; + + try + { + // Try parsing the domain name ... this might throw formatting exceptions + ParseDomainName(domainString, out tld, out sld, out subdomain, out _tldrule); + // Construct a new DomainName object and return it + result = new DomainName(tld, sld, subdomain, _tldrule); + // Return 'true' + retval = true; + } + catch + { + // Looks like something bad happened -- return 'false' + retval = false; + } + return retval; + } + + public static bool TryParseBaseDomain(string domainString, out string result) + { + var retVal = TryParse(domainString, out DomainName domain); + result = domain?.BaseDomain; + return retVal; + } + + private static void ParseDomainName(string domainString, out string TLD, out string SLD, + out string SubDomain, out TLDRule MatchingRule) + { + // Make sure domain is all lowercase + domainString = domainString.ToLower(); + + TLD = string.Empty; + SLD = string.Empty; + SubDomain = string.Empty; + MatchingRule = null; + + // If the fqdn is empty, we have a problem already + if (domainString.Trim() == string.Empty) + { + throw new ArgumentException("The domain cannot be blank"); + } + + // Next, find the matching rule: + MatchingRule = FindMatchingTLDRule(domainString); + + // At this point, no rules match, we have a problem + if (MatchingRule == null) + { + throw new FormatException("The domain does not have a recognized TLD"); + } + + // Based on the tld rule found, get the domain (and possibly the subdomain) + var tempSudomainAndDomain = string.Empty; + var tldIndex = 0; + + // First, determine what type of rule we have, and set the TLD accordingly + switch (MatchingRule.Type) + { + case TLDRule.RuleType.Normal: + tldIndex = domainString.LastIndexOf("." + MatchingRule.Name); + tempSudomainAndDomain = domainString.Substring(0, tldIndex); + TLD = domainString.Substring(tldIndex + 1); + break; + case TLDRule.RuleType.Wildcard: + // This finds the last portion of the TLD... + tldIndex = domainString.LastIndexOf("." + MatchingRule.Name); + tempSudomainAndDomain = domainString.Substring(0, tldIndex); + + // But we need to find the wildcard portion of it: + tldIndex = tempSudomainAndDomain.LastIndexOf("."); + tempSudomainAndDomain = domainString.Substring(0, tldIndex); + TLD = domainString.Substring(tldIndex + 1); + break; + case TLDRule.RuleType.Exception: + tldIndex = domainString.LastIndexOf("."); + tempSudomainAndDomain = domainString.Substring(0, tldIndex); + TLD = domainString.Substring(tldIndex + 1); + break; + } + + // See if we have a subdomain: + List lstRemainingParts = new List(tempSudomainAndDomain.Split('.')); + + // If we have 0 parts left, there is just a tld and no domain or subdomain + // If we have 1 part, it's the domain, and there is no subdomain + // If we have 2+ parts, the last part is the domain, the other parts (combined) are the subdomain + if (lstRemainingParts.Count > 0) + { + // Set the domain: + SLD = lstRemainingParts[lstRemainingParts.Count - 1]; + + // Set the subdomain, if there is one to set: + if (lstRemainingParts.Count > 1) + { + // We strip off the trailing period, too + SubDomain = tempSudomainAndDomain.Substring(0, tempSudomainAndDomain.Length - SLD.Length - 1); + } + } + } + + private static TLDRule FindMatchingTLDRule(string domainString) + { + // Split our domain into parts (based on the '.') + // ...Put these parts in a list + // ...Make sure these parts are in reverse order (we'll be checking rules from the + // right -most pat of the domain) + var lstDomainParts = domainString.Split('.').ToList(); + lstDomainParts.Reverse(); + + // Begin building our partial domain to check rules with: + var checkAgainst = string.Empty; + + // Our 'matches' collection: + var ruleMatches = new List(); + + foreach (string domainPart in lstDomainParts) + { + // Add on our next domain part: + checkAgainst = string.Format("{0}.{1}", domainPart, checkAgainst); + + // If we end in a period, strip it off: + if (checkAgainst.EndsWith(".")) + { + checkAgainst = checkAgainst.Substring(0, checkAgainst.Length - 1); + } + + var rules = Enum.GetValues(typeof(TLDRule.RuleType)).Cast(); + foreach (var rule in rules) + { + // Try to match rule: + if (TLDRulesCache.Instance.TLDRuleLists[rule].TryGetValue(checkAgainst, out TLDRule result)) + { + ruleMatches.Add(result); + } + //Debug.WriteLine(string.Format("Domain part {0} matched {1} {2} rules", + // checkAgainst, result == null ? 0 : 1, rule)); + } + } + + // Sort our matches list (longest rule wins, according to : + var results = from match in ruleMatches + orderby match.Name.Length descending + select match; + + // Take the top result (our primary match): + TLDRule primaryMatch = results.Take(1).SingleOrDefault(); + if (primaryMatch != null) + { + //Debug.WriteLine(string.Format("Looks like our match is: {0}, which is a(n) {1} rule.", + // primaryMatch.Name, primaryMatch.Type)); + } + else + { + //Debug.WriteLine(string.Format("No rules matched domain: {0}", domainString)); + } + + return primaryMatch; + } + + public class TLDRule : IComparable + { + public string Name { get; private set; } + public RuleType Type { get; private set; } + + public TLDRule(string ruleInfo) + { + // Parse the rule and set properties accordingly: + if (ruleInfo.StartsWith("*")) + { + Type = RuleType.Wildcard; + Name = ruleInfo.Substring(2); + } + else if (ruleInfo.StartsWith("!")) + { + Type = RuleType.Exception; + Name = ruleInfo.Substring(1); + } + else + { + Type = RuleType.Normal; + Name = ruleInfo; + } + } + + public int CompareTo(TLDRule other) + { + if (other == null) + { + return -1; + } + return Name.CompareTo(other.Name); + } + + public enum RuleType + { + Normal, + Wildcard, + Exception + } + } + + private class TLDRulesCache + { + private static volatile TLDRulesCache _uniqueInstance; + private static object _syncObj = new object(); + private static object _syncList = new object(); + + private IDictionary> _lstTLDRules; + + private TLDRulesCache() + { + // Initialize our internal list: + _lstTLDRules = GetTLDRules(); + } + + public static TLDRulesCache Instance + { + get + { + if (_uniqueInstance == null) + { + lock (_syncObj) + { + if (_uniqueInstance == null) + { + _uniqueInstance = new TLDRulesCache(); + } + } + } + return (_uniqueInstance); + } + } + + public IDictionary> TLDRuleLists + { + get + { + return _lstTLDRules; + } + set + { + _lstTLDRules = value; + } + } + + public static void Reset() + { + lock (_syncObj) + { + _uniqueInstance = null; + } + } + + private IDictionary> GetTLDRules() + { + var results = new Dictionary>(); + var rules = Enum.GetValues(typeof(TLDRule.RuleType)).Cast(); + foreach (var rule in rules) + { + results[rule] = new Dictionary(StringComparer.CurrentCultureIgnoreCase); + } + + var ruleStrings = ReadRulesData(); + // Strip out any lines that are: + // a.) A comment + // b.) Blank + var filteredRuleString = ruleStrings.Where(ruleString => + !ruleString.StartsWith("//") && ruleString.Trim().Length != 0); + foreach (var ruleString in filteredRuleString) + { + var result = new TLDRule(ruleString); + results[result.Type][result.Name] = result; + } + + // Return our results: + if (CoreHelpers.InDebugMode()) + { + Debug.WriteLine(string.Format("Loaded {0} rules into cache.", + results.Values.Sum(r => r.Values.Count))); + } + return results; + } + + private IEnumerable ReadRulesData() + { + var assembly = typeof(TLDRulesCache).GetTypeInfo().Assembly; + var stream = assembly.GetManifestResourceStream("Bit.Core.Resources.public_suffix_list.dat"); + string line; + using (var reader = new StreamReader(stream)) + { + while ((line = reader.ReadLine()) != null) + { + yield return line; + } + } + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Utilities/EEFLongWordList.cs b/src/Maui/Bitwarden/Core/Utilities/EEFLongWordList.cs new file mode 100644 index 000000000..a01931beb --- /dev/null +++ b/src/Maui/Bitwarden/Core/Utilities/EEFLongWordList.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Bit.Core.Utilities +{ + public class EEFLongWordList + { + private static volatile EEFLongWordList _uniqueInstance; + private static object _syncObj = new object(); + + private EEFLongWordList() + { + List = ReadData().ToList(); + } + + public static EEFLongWordList Instance + { + get + { + if (_uniqueInstance == null) + { + lock (_syncObj) + { + if (_uniqueInstance == null) + { + _uniqueInstance = new EEFLongWordList(); + } + } + } + return (_uniqueInstance); + } + } + + public List List { get; set; } + + private IEnumerable ReadData() + { + var assembly = typeof(EEFLongWordList).GetTypeInfo().Assembly; + var stream = assembly.GetManifestResourceStream("Bit.Core.Resources.eff_long_word_list.txt"); + string line; + using (var reader = new StreamReader(stream)) + { + while ((line = reader.ReadLine()) != null) + { + yield return line; + } + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Utilities/ExtendedObservableCollection.cs b/src/Maui/Bitwarden/Core/Utilities/ExtendedObservableCollection.cs new file mode 100644 index 000000000..69d0de5f8 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Utilities/ExtendedObservableCollection.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; + +namespace Bit.Core.Utilities +{ + public class ExtendedObservableCollection : ObservableCollection + { + public ExtendedObservableCollection() + : base() { } + + public ExtendedObservableCollection(IEnumerable collection) + : base(collection) { } + + public ExtendedObservableCollection(List list) + : base(list) { } + + public void AddRange(IEnumerable range) + { + foreach (var item in range) + { + Items.Add(item); + } + + OnPropertyChanged(new PropertyChangedEventArgs("Count")); + OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + public void ResetWithRange(IEnumerable range) + { + // Maybe a fix for https://forums.xamarin.com/discussion/19114/invalid-number-of-rows-in-section + // Items.Clear(); + if (Items.Count > 0) + { + var count = Items.Count; + for (var i = 0; i < count; i++) + { + Items.RemoveAt(0); + } + } + AddRange(range); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Utilities/ExtendedViewModel.cs b/src/Maui/Bitwarden/Core/Utilities/ExtendedViewModel.cs new file mode 100644 index 000000000..ed586d453 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Utilities/ExtendedViewModel.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Bit.Core.Utilities +{ + public abstract class ExtendedViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + protected bool SetProperty(ref T backingStore, T value, Action onChanged = null, + [CallerMemberName] string propertyName = "", string[] additionalPropertyNames = null) + { + if (EqualityComparer.Default.Equals(backingStore, value)) + { + return false; + } + + backingStore = value; + TriggerPropertyChanged(propertyName, additionalPropertyNames); + onChanged?.Invoke(); + return true; + } + + protected void TriggerPropertyChanged(string propertyName, string[] additionalPropertyNames = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + if (PropertyChanged != null && additionalPropertyNames != null) + { + foreach (var prop in additionalPropertyNames) + { + PropertyChanged.Invoke(this, new PropertyChangedEventArgs(prop)); + } + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Utilities/LazyResolve.cs b/src/Maui/Bitwarden/Core/Utilities/LazyResolve.cs new file mode 100644 index 000000000..1e8a37ee1 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Utilities/LazyResolve.cs @@ -0,0 +1,17 @@ +using System; + +namespace Bit.Core.Utilities +{ + public class LazyResolve : Lazy where T : class + { + public LazyResolve() + : base(() => ServiceContainer.Resolve()) + { + } + + public LazyResolve(string containerKey) + : base(() => ServiceContainer.Resolve(containerKey)) + { + } + } +} diff --git a/src/Maui/Bitwarden/Core/Utilities/OtpData.cs b/src/Maui/Bitwarden/Core/Utilities/OtpData.cs new file mode 100644 index 000000000..874aa8c3a --- /dev/null +++ b/src/Maui/Bitwarden/Core/Utilities/OtpData.cs @@ -0,0 +1,94 @@ +using System; +using System.Linq; +using Bit.Core.Enums; + +namespace Bit.Core.Utilities +{ + public struct OtpData + { + const string LABEL_SEPARATOR = ":"; + + public OtpData(string absoluteUri) + { + if (!System.Uri.TryCreate(absoluteUri, UriKind.Absolute, out var uri) + || + uri.Scheme != Constants.OtpAuthScheme) + { + throw new InvalidOperationException("Cannot create OtpData. Invalid OTP uri"); + } + + Uri = absoluteUri; + AccountName = null; + Issuer = null; + Secret = null; + Digits = null; + Period = null; + Algorithm = null; + + var escapedlabel = uri.Segments.Last(); + if (escapedlabel != "/") + { + var label = UriExtensions.UnescapeDataString(escapedlabel); + if (label.Contains(LABEL_SEPARATOR)) + { + var parts = label.Split(LABEL_SEPARATOR); + Issuer = parts[0].Trim(); + AccountName = parts[1].Trim(); + } + else + { + AccountName = label.Trim(); + } + } + + var qsParams = CoreHelpers.GetQueryParams(uri); + if (Issuer is null && qsParams.TryGetValue("issuer", out var issuer)) + { + Issuer = issuer; + } + + if (qsParams.TryGetValue("secret", out var secret)) + { + Secret = secret; + } + + if (qsParams.TryGetValue("digits", out var digitParam) + && + int.TryParse(digitParam?.Trim(), out var digits)) + { + Digits = digits; + } + + if (qsParams.TryGetValue("period", out var periodParam) + && + int.TryParse(periodParam?.Trim(), out var period) + && + period > 0) + { + Period = period; + } + + if (qsParams.TryGetValue("algorithm", out var algParam) + && + algParam?.ToLower() is string alg) + { + if (alg == "sha256") + { + Algorithm = CryptoHashAlgorithm.Sha256; + } + else if (alg == "sha512") + { + Algorithm = CryptoHashAlgorithm.Sha512; + } + } + } + + public string Uri { get; } + public string AccountName { get; } + public string Issuer { get; } + public string Secret { get; } + public int? Digits { get; } + public int? Period { get; } + public CryptoHashAlgorithm? Algorithm { get; } + } +} diff --git a/src/Maui/Bitwarden/Core/Utilities/ServiceContainer.cs b/src/Maui/Bitwarden/Core/Utilities/ServiceContainer.cs new file mode 100644 index 000000000..8cab68547 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Utilities/ServiceContainer.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Services; + +namespace Bit.Core.Utilities +{ + public static class ServiceContainer + { + public static ConcurrentDictionary RegisteredServices { get; set; } = new ConcurrentDictionary(); + public static bool Inited { get; set; } + + public static void Init(string customUserAgent = null, string clearCipherCacheKey = null, + string[] allClearCipherCacheKeys = null) + { + if (Inited) + { + return; + } + Inited = true; + + var platformUtilsService = Resolve("platformUtilsService"); + var storageService = Resolve("storageService"); + var stateService = Resolve("stateService"); + var i18nService = Resolve("i18nService"); + var messagingService = Resolve("messagingService"); + var cryptoFunctionService = Resolve("cryptoFunctionService"); + var cryptoService = Resolve("cryptoService"); + var logger = Resolve(); + + SearchService searchService = null; + + var conditionedRunner = new ConditionedAwaiterManager(); + var tokenService = new TokenService(stateService); + var apiService = new ApiService(tokenService, platformUtilsService, (extras) => + { + messagingService.Send("logout", extras); + return Task.CompletedTask; + }, customUserAgent); + var appIdService = new AppIdService(storageService); + var organizationService = new OrganizationService(stateService, apiService); + var settingsService = new SettingsService(stateService); + var fileUploadService = new FileUploadService(apiService); + var cipherService = new CipherService(cryptoService, stateService, settingsService, apiService, + fileUploadService, storageService, i18nService, () => searchService, clearCipherCacheKey, + allClearCipherCacheKeys); + var folderService = new FolderService(cryptoService, stateService, apiService, i18nService, cipherService); + var collectionService = new CollectionService(cryptoService, stateService, i18nService); + var sendService = new SendService(cryptoService, stateService, apiService, fileUploadService, i18nService, + cryptoFunctionService); + searchService = new SearchService(cipherService, sendService); + var policyService = new PolicyService(stateService, organizationService); + var keyConnectorService = new KeyConnectorService(stateService, cryptoService, tokenService, apiService, + organizationService); + var vaultTimeoutService = new VaultTimeoutService(cryptoService, stateService, platformUtilsService, + folderService, cipherService, collectionService, searchService, messagingService, tokenService, + keyConnectorService, + (extras) => + { + messagingService.Send("locked", extras); + return Task.CompletedTask; + }, + (extras) => + { + messagingService.Send("logout", extras); + return Task.CompletedTask; + }); + var syncService = new SyncService(stateService, apiService, settingsService, folderService, cipherService, + cryptoService, collectionService, organizationService, messagingService, policyService, sendService, + keyConnectorService, logger, (extras) => + { + messagingService.Send("logout", extras); + return Task.CompletedTask; + }); + var passwordGenerationService = new PasswordGenerationService(cryptoService, stateService, cryptoFunctionService, policyService); + var totpService = new TotpService(cryptoFunctionService); + var authService = new AuthService(cryptoService, cryptoFunctionService, apiService, stateService, + tokenService, appIdService, i18nService, platformUtilsService, messagingService, vaultTimeoutService, + keyConnectorService, passwordGenerationService, policyService); + var exportService = new ExportService(folderService, cipherService, cryptoService); + var auditService = new AuditService(cryptoFunctionService, apiService); + var environmentService = new EnvironmentService(apiService, stateService, conditionedRunner); + var eventService = new EventService(apiService, stateService, organizationService, cipherService); + var userVerificationService = new UserVerificationService(apiService, platformUtilsService, i18nService, + cryptoService); + var usernameGenerationService = new UsernameGenerationService(cryptoService, apiService, stateService); + var configService = new ConfigService(apiService, stateService, logger); + + Register(conditionedRunner); + Register("tokenService", tokenService); + Register("apiService", apiService); + Register("appIdService", appIdService); + Register("organizationService", organizationService); + Register("settingsService", settingsService); + Register("cipherService", cipherService); + Register("folderService", folderService); + Register("collectionService", collectionService); + Register("sendService", sendService); + Register("searchService", searchService); + Register("policyService", policyService); + Register("syncService", syncService); + Register("vaultTimeoutService", vaultTimeoutService); + Register("passwordGenerationService", passwordGenerationService); + Register("totpService", totpService); + Register("authService", authService); + Register("exportService", exportService); + Register("auditService", auditService); + Register("environmentService", environmentService); + Register("eventService", eventService); + Register("keyConnectorService", keyConnectorService); + Register("userVerificationService", userVerificationService); + Register(usernameGenerationService); + Register(configService); + } + + public static void Register(string serviceName, T obj) + { + if (!RegisteredServices.TryAdd(serviceName, obj)) + { + throw new Exception($"Service {serviceName} has already been registered."); + } + } + + public static T Resolve(string serviceName, bool dontThrow = false) + { + if (RegisteredServices.TryGetValue(serviceName, out var service)) + { + return (T)service; + } + if (dontThrow) + { + return (T)(object)null; + } + throw new Exception($"Service {serviceName} is not registered."); + } + + public static void Register(T obj) + where T : class + { + Register(typeof(T), obj); + } + + public static void Register(Type type, object obj) + { + var serviceName = GetServiceRegistrationName(type); + if (!RegisteredServices.TryAdd(serviceName, obj)) + { + throw new Exception($"Service {serviceName} has already been registered."); + } + } + + public static T Resolve() + where T : class + { + return (T)Resolve(typeof(T)); + } + + public static object Resolve(Type type) + { + var serviceName = GetServiceRegistrationName(type); + if (RegisteredServices.TryGetValue(serviceName, out var service)) + { + return service; + } + throw new Exception($"Service {serviceName} is not registered."); + } + + public static bool TryResolve(out T service) + where T : class + { + try + { + var toReturn = TryResolve(typeof(T), out var serviceObj); + service = (T)serviceObj; + return toReturn; + } + catch (Exception) + { + service = null; + return false; + } + } + + public static bool TryResolve(Type type, out object service) + { + var serviceName = GetServiceRegistrationName(type); + return RegisteredServices.TryGetValue(serviceName, out service); + } + + public static void Reset() + { + foreach (var service in RegisteredServices) + { + if (service.Value != null && service.Value is IDisposable disposableService) + { + disposableService.Dispose(); + } + } + Inited = false; + RegisteredServices.Clear(); + RegisteredServices = new ConcurrentDictionary(); + } + + /// + /// Gets the service registration name + /// + /// Type of the service + /// + /// In order to work with already register/resolve we need to maintain the naming convention + /// of camelCase without the first "I" on the services interfaces + /// e.g. "ITokenService" -> "tokenService" + /// + static string GetServiceRegistrationName(Type type) + { + var typeName = type.Name; + var sb = new StringBuilder(); + + var indexToLowerCase = 0; + if (typeName[0] == 'I' && char.IsUpper(typeName[1])) + { + // if it's an interface then we ignore the first char + // and lower case the 2nd one (index 1) + indexToLowerCase = 1; + } + sb.Append(char.ToLower(typeName[indexToLowerCase])); + sb.Append(typeName.Substring(++indexToLowerCase)); + return sb.ToString(); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Utilities/StringExtensions.cs b/src/Maui/Bitwarden/Core/Utilities/StringExtensions.cs new file mode 100644 index 000000000..a37dc9f49 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Utilities/StringExtensions.cs @@ -0,0 +1,34 @@ +using System; +using System.Globalization; +using System.Text; + +namespace Bit.Core.Utilities +{ + public static class StringExtensions + { + public static string RemoveDiacritics(this string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return text; + } + + var normalizedString = text.Normalize(NormalizationForm.FormD); + var stringBuilder = new StringBuilder(capacity: normalizedString.Length); + + for (int i = 0; i < normalizedString.Length; i++) + { + char c = normalizedString[i]; + var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); + if (unicodeCategory != UnicodeCategory.NonSpacingMark) + { + stringBuilder.Append(c); + } + } + + return stringBuilder + .ToString() + .Normalize(NormalizationForm.FormC); + } + } +} diff --git a/src/Maui/Bitwarden/Core/Utilities/TaskExtensions.cs b/src/Maui/Bitwarden/Core/Utilities/TaskExtensions.cs new file mode 100644 index 000000000..2400671f6 --- /dev/null +++ b/src/Maui/Bitwarden/Core/Utilities/TaskExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Services; + +namespace Bit.Core.Utilities +{ + public static class TaskExtensions + { + /// + /// Fires a task and ignores any exception. + /// See http://stackoverflow.com/a/22864616/344182 + /// + /// The task to be forgotten. + /// Action to be called on exception. + public static async void FireAndForget(this Task task, Action onException = null) + { + try + { + await task.ConfigureAwait(false); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + onException?.Invoke(ex); + } + } + } +} diff --git a/src/Maui/Bitwarden/Core/Utilities/UriExtensions.cs b/src/Maui/Bitwarden/Core/Utilities/UriExtensions.cs new file mode 100644 index 000000000..aee97136e --- /dev/null +++ b/src/Maui/Bitwarden/Core/Utilities/UriExtensions.cs @@ -0,0 +1,18 @@ +using System; + +namespace Bit.Core.Utilities +{ + public static class UriExtensions + { + public static string UnescapeDataString(string uriString) + { + string unescapedUri; + while ((unescapedUri = System.Uri.UnescapeDataString(uriString)) != uriString) + { + uriString = unescapedUri; + } + + return unescapedUri; + } + } +} diff --git a/src/Maui/Bitwarden/Effects/FabShadowEffect.cs b/src/Maui/Bitwarden/Effects/FabShadowEffect.cs new file mode 100644 index 000000000..2cdd37a02 --- /dev/null +++ b/src/Maui/Bitwarden/Effects/FabShadowEffect.cs @@ -0,0 +1,12 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Effects +{ + public class FabShadowEffect : RoutingEffect + { + public FabShadowEffect() + : base("Bitwarden.FabShadowEffect") + { } + } +} diff --git a/src/Maui/Bitwarden/Effects/FixedSizeEffect.cs b/src/Maui/Bitwarden/Effects/FixedSizeEffect.cs new file mode 100644 index 000000000..471151cab --- /dev/null +++ b/src/Maui/Bitwarden/Effects/FixedSizeEffect.cs @@ -0,0 +1,12 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Effects +{ + public class FixedSizeEffect : RoutingEffect + { + public FixedSizeEffect() + : base("Bitwarden.FixedSizeEffect") + { } + } +} diff --git a/src/Maui/Bitwarden/Effects/NoEmojiKeyboardEffect.cs b/src/Maui/Bitwarden/Effects/NoEmojiKeyboardEffect.cs new file mode 100644 index 000000000..b8830441b --- /dev/null +++ b/src/Maui/Bitwarden/Effects/NoEmojiKeyboardEffect.cs @@ -0,0 +1,13 @@ +using System; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Effects +{ + public class NoEmojiKeyboardEffect : RoutingEffect + { + public NoEmojiKeyboardEffect() + : base("Bitwarden.NoEmojiKeyboardEffect") + { } + } +} diff --git a/src/Maui/Bitwarden/Effects/RemoveFontPaddingEffect.cs b/src/Maui/Bitwarden/Effects/RemoveFontPaddingEffect.cs new file mode 100644 index 000000000..0e5eb29ba --- /dev/null +++ b/src/Maui/Bitwarden/Effects/RemoveFontPaddingEffect.cs @@ -0,0 +1,14 @@ +using System; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Effects +{ + public class RemoveFontPaddingEffect : RoutingEffect + { + public RemoveFontPaddingEffect() + : base("Bitwarden.RemoveFontPaddingEffect") + { } + } +} + diff --git a/src/Maui/Bitwarden/Effects/ScrollEnabledEffect.cs b/src/Maui/Bitwarden/Effects/ScrollEnabledEffect.cs new file mode 100644 index 000000000..b2311b6fd --- /dev/null +++ b/src/Maui/Bitwarden/Effects/ScrollEnabledEffect.cs @@ -0,0 +1,26 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Effects +{ + public class ScrollEnabledEffect : RoutingEffect + { + public static readonly BindableProperty IsScrollEnabledProperty = + BindableProperty.CreateAttached("IsScrollEnabled", typeof(bool), typeof(ScrollEnabledEffect), true); + + public static bool GetIsScrollEnabled(BindableObject view) + { + return (bool)view.GetValue(IsScrollEnabledProperty); + } + + public static void SetIsScrollEnabled(BindableObject view, bool value) + { + view.SetValue(IsScrollEnabledProperty, value); + } + + public ScrollEnabledEffect() + : base("Bitwarden.ScrollEnabledEffect") + { + } + } +} diff --git a/src/Maui/Bitwarden/Effects/ScrollViewContentInsetAdjustmentBehaviorEffect.cs b/src/Maui/Bitwarden/Effects/ScrollViewContentInsetAdjustmentBehaviorEffect.cs new file mode 100644 index 000000000..65e185cb0 --- /dev/null +++ b/src/Maui/Bitwarden/Effects/ScrollViewContentInsetAdjustmentBehaviorEffect.cs @@ -0,0 +1,34 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Effects +{ + public enum ScrollContentInsetAdjustmentBehavior + { + Automatic, + ScrollableAxes, + Never, + Always + } + + public class ScrollViewContentInsetAdjustmentBehaviorEffect : RoutingEffect + { + public static readonly BindableProperty ContentInsetAdjustmentBehaviorProperty = + BindableProperty.CreateAttached("ContentInsetAdjustmentBehavior", typeof(ScrollContentInsetAdjustmentBehavior), typeof(ScrollViewContentInsetAdjustmentBehaviorEffect), ScrollContentInsetAdjustmentBehavior.Automatic); + + public static ScrollContentInsetAdjustmentBehavior GetContentInsetAdjustmentBehavior(BindableObject view) + { + return (ScrollContentInsetAdjustmentBehavior)view.GetValue(ContentInsetAdjustmentBehaviorProperty); + } + + public static void SetContentInsetAdjustmentBehavior(BindableObject view, ScrollContentInsetAdjustmentBehavior value) + { + view.SetValue(ContentInsetAdjustmentBehaviorProperty, value); + } + + public ScrollViewContentInsetAdjustmentBehaviorEffect() + : base($"Bitwarden.{nameof(ScrollViewContentInsetAdjustmentBehaviorEffect)}") + { + } + } +} diff --git a/src/Maui/Bitwarden/Effects/TabBarEffect.cs b/src/Maui/Bitwarden/Effects/TabBarEffect.cs new file mode 100644 index 000000000..8e6c46544 --- /dev/null +++ b/src/Maui/Bitwarden/Effects/TabBarEffect.cs @@ -0,0 +1,12 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Effects +{ + public class TabBarEffect : RoutingEffect + { + public TabBarEffect() + : base("Bitwarden.TabBarEffect") + { } + } +} diff --git a/src/Maui/Bitwarden/Lists/DataTemplateSelectors/CustomFieldItemTemplateSelector.cs b/src/Maui/Bitwarden/Lists/DataTemplateSelectors/CustomFieldItemTemplateSelector.cs new file mode 100644 index 000000000..b6c77e050 --- /dev/null +++ b/src/Maui/Bitwarden/Lists/DataTemplateSelectors/CustomFieldItemTemplateSelector.cs @@ -0,0 +1,29 @@ +using Bit.App.Lists.ItemViewModels.CustomFields; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Lists.DataTemplateSelectors +{ + public class CustomFieldItemTemplateSelector : DataTemplateSelector + { + public DataTemplate TextTemplate { get; set; } + public DataTemplate BooleanTemplate { get; set; } + public DataTemplate LinkedTemplate { get; set; } + public DataTemplate HiddenTemplate { get; set; } + + protected override DataTemplate OnSelectTemplate(object item, BindableObject container) + { + switch (item) + { + case BooleanCustomFieldItemViewModel _: + return BooleanTemplate; + case LinkedCustomFieldItemViewModel _: + return LinkedTemplate; + case HiddenCustomFieldItemViewModel _: + return HiddenTemplate; + default: + return TextTemplate; + } + } + } +} diff --git a/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/BooleanCustomFieldItemLayout.xaml b/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/BooleanCustomFieldItemLayout.xaml new file mode 100644 index 000000000..73ec49f8a --- /dev/null +++ b/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/BooleanCustomFieldItemLayout.xaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/BooleanCustomFieldItemLayout.xaml.cs b/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/BooleanCustomFieldItemLayout.xaml.cs new file mode 100644 index 000000000..2f1e0481d --- /dev/null +++ b/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/BooleanCustomFieldItemLayout.xaml.cs @@ -0,0 +1,13 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Lists.ItemLayouts.CustomFields +{ + public partial class BooleanCustomFieldItemLayout : StackLayout + { + public BooleanCustomFieldItemLayout() + { + InitializeComponent(); + } + } +} diff --git a/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/HiddenCustomFieldItemLayout.xaml b/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/HiddenCustomFieldItemLayout.xaml new file mode 100644 index 000000000..5b5d14fa0 --- /dev/null +++ b/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/HiddenCustomFieldItemLayout.xaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/HiddenCustomFieldItemLayout.xaml.cs b/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/HiddenCustomFieldItemLayout.xaml.cs new file mode 100644 index 000000000..1cc2554e0 --- /dev/null +++ b/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/HiddenCustomFieldItemLayout.xaml.cs @@ -0,0 +1,13 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Lists.ItemLayouts.CustomFields +{ + public partial class HiddenCustomFieldItemLayout : StackLayout + { + public HiddenCustomFieldItemLayout() + { + InitializeComponent(); + } + } +} diff --git a/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/LinkedCustomFieldItemLayout.xaml b/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/LinkedCustomFieldItemLayout.xaml new file mode 100644 index 000000000..0756e3098 --- /dev/null +++ b/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/LinkedCustomFieldItemLayout.xaml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/LinkedCustomFieldItemLayout.xaml.cs b/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/LinkedCustomFieldItemLayout.xaml.cs new file mode 100644 index 000000000..98583da46 --- /dev/null +++ b/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/LinkedCustomFieldItemLayout.xaml.cs @@ -0,0 +1,13 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Lists.ItemLayouts.CustomFields +{ + public partial class LinkedCustomFieldItemLayout : StackLayout + { + public LinkedCustomFieldItemLayout() + { + InitializeComponent(); + } + } +} diff --git a/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/TextCustomFieldItemLayout.xaml b/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/TextCustomFieldItemLayout.xaml new file mode 100644 index 000000000..1b99920b5 --- /dev/null +++ b/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/TextCustomFieldItemLayout.xaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/TextCustomFieldItemLayout.xaml.cs b/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/TextCustomFieldItemLayout.xaml.cs new file mode 100644 index 000000000..e5ce04c86 --- /dev/null +++ b/src/Maui/Bitwarden/Lists/ItemLayouts/CustomFields/TextCustomFieldItemLayout.xaml.cs @@ -0,0 +1,13 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Lists.ItemLayouts.CustomFields +{ + public partial class TextCustomFieldItemLayout : StackLayout + { + public TextCustomFieldItemLayout() + { + InitializeComponent(); + } + } +} diff --git a/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/BaseCustomFieldItemViewModel.cs b/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/BaseCustomFieldItemViewModel.cs new file mode 100644 index 000000000..963eb9d31 --- /dev/null +++ b/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/BaseCustomFieldItemViewModel.cs @@ -0,0 +1,50 @@ +using System.Windows.Input; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Lists.ItemViewModels.CustomFields +{ + public abstract class BaseCustomFieldItemViewModel : ExtendedViewModel, ICustomFieldItemViewModel + { + protected FieldView _field; + protected bool _isEditing; + private string[] _additionalFieldProperties = new string[] + { + nameof(ValueText), + nameof(ShowCopyButton) + }; + + public BaseCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand) + { + _field = field; + _isEditing = isEditing; + FieldOptionsCommand = new Command(() => fieldOptionsCommand?.Execute(this)); + } + + public FieldView Field + { + get => _field; + set => SetProperty(ref _field, value, + additionalPropertyNames: new string[] + { + nameof(ValueText), + nameof(ShowCopyButton), + }); + } + + public bool IsEditing => _isEditing; + + public virtual bool ShowCopyButton => false; + + public virtual string ValueText => _field.Value; + + public ICommand FieldOptionsCommand { get; } + + public void TriggerFieldChanged() + { + TriggerPropertyChanged(nameof(Field), _additionalFieldProperties); + } + } +} diff --git a/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/BooleanCustomFieldItemViewModel.cs b/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/BooleanCustomFieldItemViewModel.cs new file mode 100644 index 000000000..81b72f68c --- /dev/null +++ b/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/BooleanCustomFieldItemViewModel.cs @@ -0,0 +1,23 @@ +using System.Windows.Input; +using Bit.Core.Models.View; + +namespace Bit.App.Lists.ItemViewModels.CustomFields +{ + public class BooleanCustomFieldItemViewModel : BaseCustomFieldItemViewModel + { + public BooleanCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand) + : base(field, isEditing, fieldOptionsCommand) + { + } + + public bool BooleanValue + { + get => bool.TryParse(Field.Value, out var boolVal) && boolVal; + set + { + Field.Value = value.ToString().ToLower(); + TriggerPropertyChanged(nameof(BooleanValue)); + } + } + } +} diff --git a/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/CustomFieldItemFactory.cs b/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/CustomFieldItemFactory.cs new file mode 100644 index 000000000..9987700e4 --- /dev/null +++ b/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/CustomFieldItemFactory.cs @@ -0,0 +1,53 @@ +using System; +using System.Windows.Input; +using Bit.App.Utilities; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.View; + +namespace Bit.App.Lists.ItemViewModels.CustomFields +{ + public interface ICustomFieldItemFactory + { + ICustomFieldItemViewModel CreateCustomFieldItem(FieldView field, + bool isEditing, + CipherView cipher, + IPasswordPromptable passwordPromptable, + ICommand copyFieldCommand, + ICommand fieldOptionsCommand); + } + + public class CustomFieldItemFactory : ICustomFieldItemFactory + { + readonly II18nService _i18nService; + readonly IEventService _eventService; + + public CustomFieldItemFactory(II18nService i18nService, IEventService eventService) + { + _i18nService = i18nService; + _eventService = eventService; + } + + public ICustomFieldItemViewModel CreateCustomFieldItem(FieldView field, + bool isEditing, + CipherView cipher, + IPasswordPromptable passwordPromptable, + ICommand copyFieldCommand, + ICommand fieldOptionsCommand) + { + switch (field.Type) + { + case FieldType.Text: + return new TextCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, copyFieldCommand); + case FieldType.Boolean: + return new BooleanCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand); + case FieldType.Hidden: + return new HiddenCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, cipher, passwordPromptable, _eventService, copyFieldCommand); + case FieldType.Linked: + return new LinkedCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, cipher, _i18nService); + default: + throw new NotImplementedException("There is no custom field item for field type " + field.Type); + } + } + } +} diff --git a/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/HiddenCustomFieldItemViewModel.cs b/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/HiddenCustomFieldItemViewModel.cs new file mode 100644 index 000000000..c88c57f7f --- /dev/null +++ b/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/HiddenCustomFieldItemViewModel.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Input; +using Bit.App.Utilities; +using Bit.Core.Abstractions; +using Bit.Core.Models.View; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Lists.ItemViewModels.CustomFields +{ + public class HiddenCustomFieldItemViewModel : BaseCustomFieldItemViewModel + { + private readonly CipherView _cipher; + private readonly IPasswordPromptable _passwordPromptable; + private readonly IEventService _eventService; + private bool _showHiddenValue; + + public HiddenCustomFieldItemViewModel(FieldView field, + bool isEditing, + ICommand fieldOptionsCommand, + CipherView cipher, + IPasswordPromptable passwordPromptable, + IEventService eventService, + ICommand copyFieldCommand) + : base(field, isEditing, fieldOptionsCommand) + { + _cipher = cipher; + _passwordPromptable = passwordPromptable; + _eventService = eventService; + + CopyFieldCommand = new Command(() => copyFieldCommand?.Execute(Field)); + ToggleHiddenValueCommand = new AsyncCommand(ToggleHiddenValueAsync, ex => + { +#if !FDROID + Microsoft.AppCenter.Crashes.Crashes.TrackError(ex); +#endif + }); + } + + public ICommand CopyFieldCommand { get; } + + public ICommand ToggleHiddenValueCommand { get; set; } + + public bool ShowHiddenValue + { + get => _showHiddenValue; + set => SetProperty(ref _showHiddenValue, value); + } + + public bool ShowViewHidden => _cipher.ViewPassword || (_isEditing && _field.NewField); + + public override bool ShowCopyButton => !_isEditing && _cipher.ViewPassword && !string.IsNullOrWhiteSpace(Field.Value); + + public async Task ToggleHiddenValueAsync() + { + if (!_isEditing && !await _passwordPromptable.PromptPasswordAsync()) + { + return; + } + + ShowHiddenValue = !ShowHiddenValue; + if (ShowHiddenValue && (!_isEditing || _cipher?.Id != null)) + { + await _eventService.CollectAsync( + Core.Enums.EventType.Cipher_ClientToggledHiddenFieldVisible, _cipher.Id); + } + } + } +} diff --git a/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/ICustomFieldItemViewModel.cs b/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/ICustomFieldItemViewModel.cs new file mode 100644 index 000000000..c671fa2b8 --- /dev/null +++ b/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/ICustomFieldItemViewModel.cs @@ -0,0 +1,13 @@ +using Bit.Core.Models.View; + +namespace Bit.App.Lists.ItemViewModels.CustomFields +{ + public interface ICustomFieldItemViewModel + { + FieldView Field { get; set; } + + bool ShowCopyButton { get; } + + void TriggerFieldChanged(); + } +} diff --git a/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/LinkedCustomFieldItemViewModel.cs b/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/LinkedCustomFieldItemViewModel.cs new file mode 100644 index 000000000..a07d1ca57 --- /dev/null +++ b/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/LinkedCustomFieldItemViewModel.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Linq; +using System.Windows.Input; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.View; + +namespace Bit.App.Lists.ItemViewModels.CustomFields +{ + public class LinkedCustomFieldItemViewModel : BaseCustomFieldItemViewModel + { + private readonly CipherView _cipher; + private readonly II18nService _i18nService; + private int _linkedFieldOptionSelectedIndex; + + public LinkedCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand, CipherView cipher, II18nService i18nService) + : base(field, isEditing, fieldOptionsCommand) + { + _cipher = cipher; + _i18nService = i18nService; + + LinkedFieldOptionSelectedIndex = Field.LinkedId.HasValue + ? LinkedFieldOptions.FindIndex(lfo => lfo.Value == Field.LinkedId.Value) + : 0; + + if (isEditing && Field.LinkedId is null) + { + field.LinkedId = LinkedFieldOptions[0].Value; + } + } + + public override string ValueText + { + get + { + var i18nKey = _cipher.LinkedFieldI18nKey(Field.LinkedId.GetValueOrDefault()); + return $"{BitwardenIcons.Link} {_i18nService.T(i18nKey)}"; + } + } + + public int LinkedFieldOptionSelectedIndex + { + get => _linkedFieldOptionSelectedIndex; + set + { + if (SetProperty(ref _linkedFieldOptionSelectedIndex, value)) + { + LinkedFieldValueChanged(); + } + } + } + + public List> LinkedFieldOptions + { + get => _cipher.LinkedFieldOptions + .Select(kvp => new KeyValuePair(_i18nService.T(kvp.Key), kvp.Value)) + .ToList(); + } + + private void LinkedFieldValueChanged() + { + if (Field != null && LinkedFieldOptionSelectedIndex > -1) + { + Field.LinkedId = LinkedFieldOptions.Find(lfo => + lfo.Value == LinkedFieldOptions[LinkedFieldOptionSelectedIndex].Value).Value; + } + } + } +} diff --git a/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/TextCustomFieldItemViewModel.cs b/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/TextCustomFieldItemViewModel.cs new file mode 100644 index 000000000..bf6192d45 --- /dev/null +++ b/src/Maui/Bitwarden/Lists/ItemViewModels/CustomFields/TextCustomFieldItemViewModel.cs @@ -0,0 +1,20 @@ +using System.Windows.Input; +using Bit.Core.Models.View; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Lists.ItemViewModels.CustomFields +{ + public class TextCustomFieldItemViewModel : BaseCustomFieldItemViewModel + { + public TextCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand, ICommand copyFieldCommand) + : base(field, isEditing, fieldOptionsCommand) + { + CopyFieldCommand = new Command(() => copyFieldCommand?.Execute(Field)); + } + + public override bool ShowCopyButton => !_isEditing && !string.IsNullOrWhiteSpace(Field.Value); + + public ICommand CopyFieldCommand { get; } + } +} diff --git a/src/Maui/Bitwarden/MauiProgram.cs b/src/Maui/Bitwarden/MauiProgram.cs new file mode 100644 index 000000000..6959b152b --- /dev/null +++ b/src/Maui/Bitwarden/MauiProgram.cs @@ -0,0 +1,40 @@ +using System; +using CommunityToolkit.Maui; +using FFImageLoading.Maui; +using Microsoft.Extensions.Logging; +using Microsoft.Maui.Controls.Compatibility.Hosting; +using Microsoft.Maui.Controls.Hosting; +using Microsoft.Maui.Hosting; +using SkiaSharp.Views.Maui.Controls.Hosting; +using ZXing.Net.Maui; +using ZXing.Net.Maui.Controls; + +namespace Bit.App; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .UseMauiCommunityToolkit() + .UseMauiCompatibility() + .UseBarcodeReader() + .UseSkiaSharp() + .UseFFImageLoading() + //.ConfigureEffects(effectsBuilder) + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); + }); + +#if DEBUG + builder.Logging.AddDebug(); +#endif + + return builder.Build(); + } +} + diff --git a/src/Maui/Bitwarden/Models/AppOptions.cs b/src/Maui/Bitwarden/Models/AppOptions.cs new file mode 100644 index 000000000..58fe79d49 --- /dev/null +++ b/src/Maui/Bitwarden/Models/AppOptions.cs @@ -0,0 +1,56 @@ +using System; +using Bit.Core.Enums; +using Bit.Core.Utilities; + +namespace Bit.App.Models +{ + public class AppOptions + { + public bool MyVaultTile { get; set; } + public bool GeneratorTile { get; set; } + public bool FromAutofillFramework { get; set; } + public CipherType? FillType { get; set; } + public string Uri { get; set; } + public CipherType? SaveType { get; set; } + public string SaveName { get; set; } + public string SaveUsername { get; set; } + public string SavePassword { get; set; } + public string SaveCardName { get; set; } + public string SaveCardNumber { get; set; } + public string SaveCardExpMonth { get; set; } + public string SaveCardExpYear { get; set; } + public string SaveCardCode { get; set; } + public bool IosExtension { get; set; } + public Tuple CreateSend { get; set; } + public bool CopyInsteadOfShareAfterSaving { get; set; } + public bool HideAccountSwitcher { get; set; } + public OtpData? OtpData { get; set; } + + public void SetAllFrom(AppOptions o) + { + if (o == null) + { + return; + } + MyVaultTile = o.MyVaultTile; + GeneratorTile = o.GeneratorTile; + FromAutofillFramework = o.FromAutofillFramework; + FillType = o.FillType; + Uri = o.Uri; + SaveType = o.SaveType; + SaveName = o.SaveName; + SaveUsername = o.SaveUsername; + SavePassword = o.SavePassword; + SaveCardName = o.SaveCardName; + SaveCardNumber = o.SaveCardNumber; + SaveCardExpMonth = o.SaveCardExpMonth; + SaveCardExpYear = o.SaveCardExpYear; + SaveCardCode = o.SaveCardCode; + IosExtension = o.IosExtension; + CreateSend = o.CreateSend; + CopyInsteadOfShareAfterSaving = o.CopyInsteadOfShareAfterSaving; + HideAccountSwitcher = o.HideAccountSwitcher; + OtpData = o.OtpData; + } + } +} diff --git a/src/Maui/Bitwarden/Models/DialogDetails.cs b/src/Maui/Bitwarden/Models/DialogDetails.cs new file mode 100644 index 000000000..a143388d4 --- /dev/null +++ b/src/Maui/Bitwarden/Models/DialogDetails.cs @@ -0,0 +1,12 @@ +namespace Bit.App.Models +{ + public class DialogDetails + { + public string Text { get; set; } + public string Title { get; set; } + public string ConfirmText { get; set; } + public string CancelText { get; set; } + public string Type { get; set; } + public int DialogId { get; set; } + } +} diff --git a/src/Maui/Bitwarden/Models/NotificationData.cs b/src/Maui/Bitwarden/Models/NotificationData.cs new file mode 100644 index 000000000..3ddd758d9 --- /dev/null +++ b/src/Maui/Bitwarden/Models/NotificationData.cs @@ -0,0 +1,23 @@ +using System; +namespace Bit.App.Models +{ + public abstract class BaseNotificationData + { + public abstract string Type { get; } + + public string Id { get; set; } + } + + public class PasswordlessNotificationData : BaseNotificationData + { + public const string TYPE = "passwordlessNotificationData"; + + public override string Type => TYPE; + + public int TimeoutInMinutes { get; set; } + + public string UserEmail { get; set; } + } + +} + diff --git a/src/Maui/Bitwarden/Models/PlatformCulture.cs b/src/Maui/Bitwarden/Models/PlatformCulture.cs new file mode 100644 index 000000000..ec9f0b1d5 --- /dev/null +++ b/src/Maui/Bitwarden/Models/PlatformCulture.cs @@ -0,0 +1,39 @@ +using System; + +namespace Bit.App.Models +{ + public class PlatformCulture + { + public PlatformCulture(string platformCultureString) + { + if (string.IsNullOrWhiteSpace(platformCultureString)) + { + throw new ArgumentException("Expected culture identifier.", nameof(platformCultureString)); + } + + // .NET expects dash, not underscore + PlatformString = platformCultureString.Replace("_", "-"); + var dashIndex = PlatformString.IndexOf("-", StringComparison.Ordinal); + if (dashIndex > 0) + { + var parts = PlatformString.Split('-'); + LanguageCode = parts[0]; + LocaleCode = parts[1]; + } + else + { + LanguageCode = PlatformString; + LocaleCode = string.Empty; + } + } + + public string PlatformString { get; private set; } + public string LanguageCode { get; private set; } + public string LocaleCode { get; private set; } + + public override string ToString() + { + return PlatformString; + } + } +} diff --git a/src/Maui/Bitwarden/Pages/Accounts/BaseChangePasswordViewModel.cs b/src/Maui/Bitwarden/Pages/Accounts/BaseChangePasswordViewModel.cs new file mode 100644 index 000000000..9cae0ba59 --- /dev/null +++ b/src/Maui/Bitwarden/Pages/Accounts/BaseChangePasswordViewModel.cs @@ -0,0 +1,176 @@ +using System.Text; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; + +namespace Bit.App.Pages +{ + public class BaseChangePasswordViewModel : BaseViewModel + { + protected readonly IPlatformUtilsService _platformUtilsService; + protected readonly IStateService _stateService; + protected readonly IPolicyService _policyService; + protected readonly IPasswordGenerationService _passwordGenerationService; + protected readonly II18nService _i18nService; + protected readonly ICryptoService _cryptoService; + protected readonly IDeviceActionService _deviceActionService; + protected readonly IApiService _apiService; + protected readonly ISyncService _syncService; + + private bool _showPassword; + private bool _isPolicyInEffect; + private string _policySummary; + private MasterPasswordPolicyOptions _policy; + + protected BaseChangePasswordViewModel() + { + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _stateService = ServiceContainer.Resolve("stateService"); + _policyService = ServiceContainer.Resolve("policyService"); + _passwordGenerationService = + ServiceContainer.Resolve("passwordGenerationService"); + _i18nService = ServiceContainer.Resolve("i18nService"); + _cryptoService = ServiceContainer.Resolve("cryptoService"); + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _apiService = ServiceContainer.Resolve("apiService"); + _syncService = ServiceContainer.Resolve("syncService"); + } + + public bool ShowPassword + { + get => _showPassword; + set => SetProperty(ref _showPassword, value, + additionalPropertyNames: new[] + { + nameof(ShowPasswordIcon), + nameof(PasswordVisibilityAccessibilityText) + }); + } + + public bool IsPolicyInEffect + { + get => _isPolicyInEffect; + set => SetProperty(ref _isPolicyInEffect, value); + } + + public string PolicySummary + { + get => _policySummary; + set => SetProperty(ref _policySummary, value); + } + + public MasterPasswordPolicyOptions Policy + { + get => _policy; + set => SetProperty(ref _policy, value); + } + + public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; + public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow; + public string MasterPassword { get; set; } + public string ConfirmMasterPassword { get; set; } + public string Hint { get; set; } + + public virtual async Task InitAsync(bool forceSync = false) + { + if (forceSync) + { + var task = Task.Run(async () => await _syncService.FullSyncAsync(true)); + await task.ContinueWith(async (t) => await CheckPasswordPolicy()); + } + else + { + await CheckPasswordPolicy(); + } + } + + private async Task CheckPasswordPolicy() + { + Policy = await _policyService.GetMasterPasswordPolicyOptions(); + IsPolicyInEffect = Policy?.InEffect() ?? false; + if (!IsPolicyInEffect) + { + return; + } + + var bullet = "\n" + "".PadLeft(4) + "\u2022 "; + var sb = new StringBuilder(); + sb.Append(_i18nService.T("MasterPasswordPolicyInEffect")); + if (Policy.MinComplexity > 0) + { + sb.Append(bullet) + .Append(string.Format(_i18nService.T("PolicyInEffectMinComplexity"), Policy.MinComplexity)); + } + if (Policy.MinLength > 0) + { + sb.Append(bullet).Append(string.Format(_i18nService.T("PolicyInEffectMinLength"), Policy.MinLength)); + } + if (Policy.RequireUpper) + { + sb.Append(bullet).Append(_i18nService.T("PolicyInEffectUppercase")); + } + if (Policy.RequireLower) + { + sb.Append(bullet).Append(_i18nService.T("PolicyInEffectLowercase")); + } + if (Policy.RequireNumbers) + { + sb.Append(bullet).Append(_i18nService.T("PolicyInEffectNumbers")); + } + if (Policy.RequireSpecial) + { + sb.Append(bullet).Append(string.Format(_i18nService.T("PolicyInEffectSpecial"), "!@#$%^&*")); + } + PolicySummary = sb.ToString(); + } + + protected async Task ValidateMasterPasswordAsync() + { + if (Connectivity.NetworkAccess == NetworkAccess.None) + { + await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, + AppResources.InternetConnectionRequiredTitle, AppResources.Ok); + return false; + } + if (string.IsNullOrWhiteSpace(MasterPassword)) + { + await _platformUtilsService.ShowDialogAsync( + string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword), + AppResources.AnErrorHasOccurred, AppResources.Ok); + return false; + } + if (IsPolicyInEffect) + { + var userInputs = _passwordGenerationService.GetPasswordStrengthUserInput(await _stateService.GetEmailAsync()); + var passwordStrength = _passwordGenerationService.PasswordStrength(MasterPassword, userInputs); + if (!await _policyService.EvaluateMasterPassword(passwordStrength.Score, MasterPassword, Policy)) + { + await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordPolicyValidationMessage, + AppResources.MasterPasswordPolicyValidationTitle, AppResources.Ok); + return false; + } + } + else + { + if (MasterPassword.Length < Constants.MasterPasswordMinimumChars) + { + await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.MasterPasswordLengthValMessageX, Constants.MasterPasswordMinimumChars), + AppResources.MasterPasswordPolicyValidationTitle, AppResources.Ok); + return false; + } + } + if (MasterPassword != ConfirmMasterPassword) + { + await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordConfirmationValMessage, + AppResources.AnErrorHasOccurred, AppResources.Ok); + return false; + } + + return true; + } + } +} diff --git a/src/Maui/Bitwarden/Pages/Accounts/DeleteAccountPage.xaml b/src/Maui/Bitwarden/Pages/Accounts/DeleteAccountPage.xaml new file mode 100644 index 000000000..877e4c36b --- /dev/null +++ b/src/Maui/Bitwarden/Pages/Accounts/DeleteAccountPage.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + diff --git a/src/Maui/Bitwarden/Pages/Accounts/LoginSsoPage.xaml.cs b/src/Maui/Bitwarden/Pages/Accounts/LoginSsoPage.xaml.cs new file mode 100644 index 000000000..d8d83903e --- /dev/null +++ b/src/Maui/Bitwarden/Pages/Accounts/LoginSsoPage.xaml.cs @@ -0,0 +1,125 @@ +using System; +using System.Threading.Tasks; +using Bit.App.Models; +using Bit.App.Utilities; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Pages +{ + public partial class LoginSsoPage : BaseContentPage + { + private readonly IVaultTimeoutService _vaultTimeoutService; + private readonly LoginSsoPageViewModel _vm; + private readonly AppOptions _appOptions; + + private AppOptions _appOptionsCopy; + + public LoginSsoPage(AppOptions appOptions = null) + { + _vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); + _appOptions = appOptions; + InitializeComponent(); + _vm = BindingContext as LoginSsoPageViewModel; + _vm.Page = this; + _vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync()); + _vm.StartSetPasswordAction = () => + Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync()); + _vm.SsoAuthSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SsoAuthSuccessAsync()); + _vm.UpdateTempPasswordAction = + () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync()); + _vm.CloseAction = async () => + { + await Navigation.PopModalAsync(); + }; + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + if (Device.RuntimePlatform == Device.Android) + { + ToolbarItems.RemoveAt(0); + } + } + + protected override async void OnAppearing() + { + base.OnAppearing(); + await _vm.InitAsync(); + if (string.IsNullOrWhiteSpace(_vm.OrgIdentifier)) + { + RequestFocus(_orgIdentifier); + } + } + + private void CopyAppOptions() + { + if (_appOptions != null) + { + // create an object copy of _appOptions to persist values when app is exited during web auth flow + _appOptionsCopy = new AppOptions(); + _appOptionsCopy.SetAllFrom(_appOptions); + } + } + + private void RestoreAppOptionsFromCopy() + { + if (_appOptions != null) + { + // restore values to original readonly _appOptions object from copy + _appOptions.SetAllFrom(_appOptionsCopy); + _appOptionsCopy = null; + } + } + + private void LogIn_Clicked(object sender, EventArgs e) + { + if (DoOnce()) + { + CopyAppOptions(); + _vm.LogInCommand.Execute(null); + } + } + + private void Close_Clicked(object sender, EventArgs e) + { + if (DoOnce()) + { + _vm.CloseAction(); + } + } + + private async Task StartTwoFactorAsync() + { + RestoreAppOptionsFromCopy(); + var page = new TwoFactorPage(true, _appOptions, _vm.OrgIdentifier); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + + private async Task StartSetPasswordAsync() + { + RestoreAppOptionsFromCopy(); + var page = new SetPasswordPage(_appOptions, _vm.OrgIdentifier); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + + private async Task UpdateTempPasswordAsync() + { + var page = new UpdateTempPasswordPage(); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + + private async Task SsoAuthSuccessAsync() + { + RestoreAppOptionsFromCopy(); + await AppHelpers.ClearPreviousPage(); + if (await _vaultTimeoutService.IsLockedAsync()) + { + Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions)); + } + else + { + Application.Current.MainPage = new TabsPage(_appOptions, null); + } + } + } +} diff --git a/src/Maui/Bitwarden/Pages/Accounts/LoginSsoPageViewModel.cs b/src/Maui/Bitwarden/Pages/Accounts/LoginSsoPageViewModel.cs new file mode 100644 index 000000000..cb5fed7b5 --- /dev/null +++ b/src/Maui/Bitwarden/Pages/Accounts/LoginSsoPageViewModel.cs @@ -0,0 +1,260 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using System.Windows.Input; +using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.App.Utilities; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; +using Bit.App.Utilities; + +namespace Bit.App.Pages +{ + public class LoginSsoPageViewModel : BaseViewModel + { + private const string REDIRECT_URI = "bitwarden://sso-callback"; + + private readonly IDeviceActionService _deviceActionService; + private readonly IAuthService _authService; + private readonly ISyncService _syncService; + private readonly IApiService _apiService; + private readonly IPasswordGenerationService _passwordGenerationService; + private readonly ICryptoFunctionService _cryptoFunctionService; + private readonly IPlatformUtilsService _platformUtilsService; + private readonly IStateService _stateService; + private readonly ILogger _logger; + private readonly IOrganizationService _organizationService; + + private string _orgIdentifier; + + public LoginSsoPageViewModel() + { + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _authService = ServiceContainer.Resolve("authService"); + _syncService = ServiceContainer.Resolve("syncService"); + _apiService = ServiceContainer.Resolve("apiService"); + _passwordGenerationService = + ServiceContainer.Resolve("passwordGenerationService"); + _cryptoFunctionService = ServiceContainer.Resolve("cryptoFunctionService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _stateService = ServiceContainer.Resolve("stateService"); + _logger = ServiceContainer.Resolve("logger"); + _organizationService = ServiceContainer.Resolve(); + + + PageTitle = AppResources.Bitwarden; + LogInCommand = new AsyncCommand(LogInAsync, allowsMultipleExecutions: false); + } + + public string OrgIdentifier + { + get => _orgIdentifier; + set => SetProperty(ref _orgIdentifier, value); + } + + public ICommand LogInCommand { get; } + public Action StartTwoFactorAction { get; set; } + public Action StartSetPasswordAction { get; set; } + public Action SsoAuthSuccessAction { get; set; } + public Action CloseAction { get; set; } + public Action UpdateTempPasswordAction { get; set; } + + public async Task InitAsync() + { + try + { + if (await TryClaimedDomainLogin()) + { + return; + } + + if (string.IsNullOrWhiteSpace(OrgIdentifier)) + { + OrgIdentifier = await _stateService.GetRememberedOrgIdentifierAsync(); + } + } + catch (Exception ex) + { + _logger.Exception(ex); + } + finally + { + await _deviceActionService.HideLoadingAsync(); + } + } + + public async Task LogInAsync() + { + try + { + if (Connectivity.NetworkAccess == NetworkAccess.None) + { + await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, + AppResources.InternetConnectionRequiredTitle); + return; + } + if (string.IsNullOrWhiteSpace(OrgIdentifier)) + { + await _platformUtilsService.ShowDialogAsync( + string.Format(AppResources.ValidationFieldRequired, AppResources.OrgIdentifier), + AppResources.AnErrorHasOccurred, + AppResources.Ok); + return; + } + + await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn); + + var response = await _apiService.PreValidateSso(OrgIdentifier); + + if (string.IsNullOrWhiteSpace(response?.Token)) + { + _logger.Error(response is null ? "Login SSO Error: response is null" : "Login SSO Error: response.Token is null or whitespace"); + await _deviceActionService.HideLoadingAsync(); + await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError); + return; + } + + var ssoToken = response.Token; + + var passwordOptions = PasswordGenerationOptions.CreateDefault + .WithLength(64); + + var codeVerifier = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions); + var codeVerifierHash = await _cryptoFunctionService.HashAsync(codeVerifier, CryptoHashAlgorithm.Sha256); + var codeChallenge = CoreHelpers.Base64UrlEncode(codeVerifierHash); + + var state = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions); + + var url = _apiService.IdentityBaseUrl + "/connect/authorize?" + + "client_id=" + _platformUtilsService.GetClientType().GetString() + "&" + + "redirect_uri=" + Uri.EscapeDataString(REDIRECT_URI) + "&" + + "response_type=code&scope=api%20offline_access&" + + "state=" + state + "&code_challenge=" + codeChallenge + "&" + + "code_challenge_method=S256&response_mode=query&" + + "domain_hint=" + Uri.EscapeDataString(OrgIdentifier) + "&" + + "ssoToken=" + Uri.EscapeDataString(ssoToken); + + WebAuthenticatorResult authResult = null; + + authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url), + new Uri(REDIRECT_URI)); + + + var code = GetResultCode(authResult, state); + if (!string.IsNullOrEmpty(code)) + { + await LogIn(code, codeVerifier, OrgIdentifier); + } + else + { + await _deviceActionService.HideLoadingAsync(); + await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError, + AppResources.AnErrorHasOccurred); + } + } + catch (ApiException e) + { + _logger.Exception(e); + await _deviceActionService.HideLoadingAsync(); + await _platformUtilsService.ShowDialogAsync(e?.Error?.GetSingleMessage() ?? AppResources.LoginSsoError, + AppResources.AnErrorHasOccurred); + } + catch (TaskCanceledException) + { + // user canceled + await _deviceActionService.HideLoadingAsync(); + } + catch (Exception ex) + { + _logger.Exception(ex); + await _deviceActionService.HideLoadingAsync(); + await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred); + } + } + + private string GetResultCode(WebAuthenticatorResult authResult, string state) + { + string code = null; + if (authResult != null) + { + authResult.Properties.TryGetValue("state", out var resultState); + if (resultState == state) + { + authResult.Properties.TryGetValue("code", out var resultCode); + code = resultCode; + } + } + return code; + } + + private async Task LogIn(string code, string codeVerifier, string orgId) + { + try + { + var response = await _authService.LogInSsoAsync(code, codeVerifier, REDIRECT_URI, orgId); + await AppHelpers.ResetInvalidUnlockAttemptsAsync(); + await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier); + await _deviceActionService.HideLoadingAsync(); + if (response.TwoFactor) + { + StartTwoFactorAction?.Invoke(); + } + else if (response.ResetMasterPassword) + { + StartSetPasswordAction?.Invoke(); + } + else if (response.ForcePasswordReset) + { + UpdateTempPasswordAction?.Invoke(); + } + else + { + var task = Task.Run(async () => await _syncService.FullSyncAsync(true)); + SsoAuthSuccessAction?.Invoke(); + } + } + catch (Exception e) + { + await _deviceActionService.HideLoadingAsync(); + await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError, + AppResources.AnErrorHasOccurred); + } + } + + private async Task TryClaimedDomainLogin() + { + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.Loading); + var userEmail = await _stateService.GetPreLoginEmailAsync(); + var claimedDomainOrgDetails = await _organizationService.GetClaimedOrganizationDomainAsync(userEmail); + await _deviceActionService.HideLoadingAsync(); + + if (claimedDomainOrgDetails == null || !claimedDomainOrgDetails.SsoAvailable) + { + return false; + } + + if (string.IsNullOrEmpty(claimedDomainOrgDetails.OrganizationIdentifier)) + { + await _platformUtilsService.ShowDialogAsync(AppResources.OrganizationSsoIdentifierRequired, AppResources.AnErrorHasOccurred); + return false; + } + + OrgIdentifier = claimedDomainOrgDetails.OrganizationIdentifier; + await LogInAsync(); + return true; + } + catch (Exception ex) + { + HandleException(ex); + } + + return false; + } + } +} diff --git a/src/Maui/Bitwarden/Pages/Accounts/RegisterPage.xaml b/src/Maui/Bitwarden/Pages/Accounts/RegisterPage.xaml new file mode 100644 index 000000000..452fea8c6 --- /dev/null +++ b/src/Maui/Bitwarden/Pages/Accounts/RegisterPage.xaml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Maui/Bitwarden/Pages/Accounts/RegisterPage.xaml.cs b/src/Maui/Bitwarden/Pages/Accounts/RegisterPage.xaml.cs new file mode 100644 index 000000000..2cc4ff366 --- /dev/null +++ b/src/Maui/Bitwarden/Pages/Accounts/RegisterPage.xaml.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Pages +{ + public partial class RegisterPage : BaseContentPage + { + private readonly RegisterPageViewModel _vm; + + private bool _inputFocused; + + public RegisterPage(HomePage homePage) + { + InitializeComponent(); + _vm = BindingContext as RegisterPageViewModel; + _vm.Page = this; + _vm.RegistrationSuccess = () => Device.BeginInvokeOnMainThread(async () => await RegistrationSuccessAsync(homePage)); + _vm.CloseAction = async () => + { + await Navigation.PopModalAsync(); + }; + MasterPasswordEntry = _masterPassword; + ConfirmMasterPasswordEntry = _confirmMasterPassword; + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + if (Device.RuntimePlatform == Device.Android) + { + ToolbarItems.RemoveAt(0); + } + + _email.ReturnType = ReturnType.Next; + _email.ReturnCommand = new Command(() => _masterPassword.Focus()); + _masterPassword.ReturnType = ReturnType.Next; + _masterPassword.ReturnCommand = new Command(() => _confirmMasterPassword.Focus()); + _confirmMasterPassword.ReturnType = ReturnType.Next; + _confirmMasterPassword.ReturnCommand = new Command(() => _hint.Focus()); + } + + public Entry MasterPasswordEntry { get; set; } + public Entry ConfirmMasterPasswordEntry { get; set; } + + protected override void OnAppearing() + { + base.OnAppearing(); + if (!_inputFocused) + { + RequestFocus(_email); + _inputFocused = true; + } + } + + private async void Submit_Clicked(object sender, EventArgs e) + { + if (DoOnce()) + { + await _vm.SubmitAsync(); + } + } + + private async Task RegistrationSuccessAsync(HomePage homePage) + { + if (homePage != null) + { + await homePage.DismissRegisterPageAndLogInAsync(_vm.Email); + } + } + + private void Close_Clicked(object sender, EventArgs e) + { + if (DoOnce()) + { + _vm.CloseAction(); + } + } + } +} diff --git a/src/Maui/Bitwarden/Pages/Accounts/RegisterPageViewModel.cs b/src/Maui/Bitwarden/Pages/Accounts/RegisterPageViewModel.cs new file mode 100644 index 000000000..1cfaa51df --- /dev/null +++ b/src/Maui/Bitwarden/Pages/Accounts/RegisterPageViewModel.cs @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Windows.Input; +using Bit.App.Abstractions; +using Bit.App.Controls; +using Bit.App.Resources; +using Bit.App.Utilities; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Request; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Pages +{ + public class RegisterPageViewModel : CaptchaProtectedViewModel, IPasswordStrengthable + { + private readonly IDeviceActionService _deviceActionService; + private readonly II18nService _i18nService; + private readonly IEnvironmentService _environmentService; + private readonly IAuditService _auditService; + private readonly IApiService _apiService; + private readonly ICryptoService _cryptoService; + private readonly IPlatformUtilsService _platformUtilsService; + private string _email; + private string _masterPassword; + private bool _showPassword; + private bool _acceptPolicies; + private bool _checkExposedMasterPassword; + + public RegisterPageViewModel() + { + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _apiService = ServiceContainer.Resolve("apiService"); + _cryptoService = ServiceContainer.Resolve("cryptoService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _i18nService = ServiceContainer.Resolve("i18nService"); + _environmentService = ServiceContainer.Resolve("environmentService"); + _auditService = ServiceContainer.Resolve(); + + PageTitle = AppResources.CreateAccount; + TogglePasswordCommand = new Command(TogglePassword); + ToggleConfirmPasswordCommand = new Command(ToggleConfirmPassword); + SubmitCommand = new Command(async () => await SubmitAsync()); + ShowTerms = !_platformUtilsService.IsSelfHost(); + PasswordStrengthViewModel = new PasswordStrengthViewModel(this); + CheckExposedMasterPassword = true; + } + + public ICommand PoliciesClickCommand => new Command((url) => + { + _platformUtilsService.LaunchUri(url); + }); + + public bool ShowPassword + { + get => _showPassword; + set => SetProperty(ref _showPassword, value, + additionalPropertyNames: new string[] + { + nameof(ShowPasswordIcon), + nameof(PasswordVisibilityAccessibilityText) + }); + } + + public bool AcceptPolicies + { + get => _acceptPolicies; + set => SetProperty(ref _acceptPolicies, value); + } + + public bool CheckExposedMasterPassword + { + get => _checkExposedMasterPassword; + set => SetProperty(ref _checkExposedMasterPassword, value); + } + + public string MasterPassword + { + get => _masterPassword; + set + { + SetProperty(ref _masterPassword, value); + PasswordStrengthViewModel.CalculatePasswordStrength(); + } + } + + public string Email + { + get => _email; + set => SetProperty(ref _email, value); + } + + public string Password => MasterPassword; + public List UserInputs => PasswordStrengthViewModel.GetPasswordStrengthUserInput(Email); + public string MasterPasswordMininumCharactersDescription => string.Format(AppResources.YourMasterPasswordCannotBeRecoveredIfYouForgetItXCharactersMinimum, + Constants.MasterPasswordMinimumChars); + public PasswordStrengthViewModel PasswordStrengthViewModel { get; } + public bool ShowTerms { get; set; } + public Command SubmitCommand { get; } + public Command TogglePasswordCommand { get; } + public Command ToggleConfirmPasswordCommand { get; } + public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; + public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow; + public string Name { get; set; } + public string ConfirmMasterPassword { get; set; } + public string Hint { get; set; } + public Action RegistrationSuccess { get; set; } + public Action CloseAction { get; set; } + protected override II18nService i18nService => _i18nService; + protected override IEnvironmentService environmentService => _environmentService; + protected override IDeviceActionService deviceActionService => _deviceActionService; + protected override IPlatformUtilsService platformUtilsService => _platformUtilsService; + + public async Task SubmitAsync(bool showLoading = true) + { + if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None) + { + await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, + AppResources.InternetConnectionRequiredTitle, AppResources.Ok); + return; + } + if (string.IsNullOrWhiteSpace(Email)) + { + await _platformUtilsService.ShowDialogAsync( + string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress), + AppResources.AnErrorHasOccurred, + AppResources.Ok); + return; + } + if (!Email.Contains("@")) + { + await _platformUtilsService.ShowDialogAsync(AppResources.InvalidEmail, AppResources.AnErrorHasOccurred, + AppResources.Ok); + return; + } + if (string.IsNullOrWhiteSpace(MasterPassword)) + { + await _platformUtilsService.ShowDialogAsync( + string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword), + AppResources.AnErrorHasOccurred, + AppResources.Ok); + return; + } + if (MasterPassword.Length < Constants.MasterPasswordMinimumChars) + { + await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.MasterPasswordLengthValMessageX, Constants.MasterPasswordMinimumChars), + AppResources.AnErrorHasOccurred, AppResources.Ok); + return; + } + if (MasterPassword != ConfirmMasterPassword) + { + await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordConfirmationValMessage, + AppResources.AnErrorHasOccurred, AppResources.Ok); + return; + } + if (ShowTerms && !AcceptPolicies) + { + await _platformUtilsService.ShowDialogAsync(AppResources.AcceptPoliciesError, + AppResources.AnErrorHasOccurred, AppResources.Ok); + return; + } + if (await IsPasswordWeakOrExposed()) + { + return; + } + + if (showLoading) + { + await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount); + } + + Name = string.IsNullOrWhiteSpace(Name) ? null : Name; + Email = Email.Trim().ToLower(); + var kdfConfig = new KdfConfig(KdfType.PBKDF2_SHA256, Constants.Pbkdf2Iterations, null, null); + var key = await _cryptoService.MakeKeyAsync(MasterPassword, Email, kdfConfig); + var encKey = await _cryptoService.MakeEncKeyAsync(key); + var hashedPassword = await _cryptoService.HashPasswordAsync(MasterPassword, key); + var keys = await _cryptoService.MakeKeyPairAsync(encKey.Item1); + var request = new RegisterRequest + { + Email = Email, + Name = Name, + MasterPasswordHash = hashedPassword, + MasterPasswordHint = Hint, + Key = encKey.Item2.EncryptedString, + Kdf = kdfConfig.Type, + KdfIterations = kdfConfig.Iterations, + KdfMemory = kdfConfig.Memory, + KdfParallelism = kdfConfig.Parallelism, + Keys = new KeysRequest + { + PublicKey = keys.Item1, + EncryptedPrivateKey = keys.Item2.EncryptedString + }, + CaptchaResponse = _captchaToken, + }; + + // TODO: org invite? + + try + { + await _apiService.PostRegisterAsync(request); + await _deviceActionService.HideLoadingAsync(); + _platformUtilsService.ShowToast("success", null, AppResources.AccountCreated, + new System.Collections.Generic.Dictionary + { + ["longDuration"] = true + }); + RegistrationSuccess?.Invoke(); + } + catch (ApiException e) + { + if (e?.Error != null && e.Error.CaptchaRequired) + { + if (await HandleCaptchaAsync(e.Error.CaptchaSiteKey)) + { + await SubmitAsync(false); + _captchaToken = null; + } + return; + } + await _deviceActionService.HideLoadingAsync(); + if (e?.Error != null) + { + await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(), + AppResources.AnErrorHasOccurred, AppResources.Ok); + } + } + } + + public void TogglePassword() + { + ShowPassword = !ShowPassword; + var entry = (Page as RegisterPage).MasterPasswordEntry; + entry.Focus(); + entry.CursorPosition = String.IsNullOrEmpty(MasterPassword) ? 0 : MasterPassword.Length; + } + + public void ToggleConfirmPassword() + { + ShowPassword = !ShowPassword; + var entry = (Page as RegisterPage).ConfirmMasterPasswordEntry; + entry.Focus(); + entry.CursorPosition = String.IsNullOrEmpty(ConfirmMasterPassword) ? 0 : ConfirmMasterPassword.Length; + } + + private async Task IsPasswordWeakOrExposed() + { + try + { + var title = string.Empty; + var message = string.Empty; + var exposedPassword = CheckExposedMasterPassword ? await _auditService.PasswordLeakedAsync(MasterPassword) > 0 : false; + var weakPassword = PasswordStrengthViewModel.PasswordStrengthLevel <= PasswordStrengthLevel.Weak; + + if (exposedPassword && weakPassword) + { + title = AppResources.WeakAndExposedMasterPassword; + message = AppResources.WeakPasswordIdentifiedAndFoundInADataBreachAlertDescription; + } + else if (exposedPassword) + { + title = AppResources.ExposedMasterPassword; + message = AppResources.PasswordFoundInADataBreachAlertDescription; + } + else if (weakPassword) + { + title = AppResources.WeakMasterPassword; + message = AppResources.WeakPasswordIdentifiedUseAStrongPasswordToProtectYourAccount; + } + + if (exposedPassword || weakPassword) + { + return !await _platformUtilsService.ShowDialogAsync(message, title, AppResources.Yes, AppResources.No); + } + } + catch (Exception ex) + { + HandleException(ex); + } + + return false; + } + } +} diff --git a/src/Maui/Bitwarden/Pages/Accounts/RemoveMasterPasswordPage.xaml b/src/Maui/Bitwarden/Pages/Accounts/RemoveMasterPasswordPage.xaml new file mode 100644 index 000000000..a7e8eb422 --- /dev/null +++ b/src/Maui/Bitwarden/Pages/Accounts/RemoveMasterPasswordPage.xaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Maui/Bitwarden/Pages/Accounts/TwoFactorPage.xaml.cs b/src/Maui/Bitwarden/Pages/Accounts/TwoFactorPage.xaml.cs new file mode 100644 index 000000000..26b0280d0 --- /dev/null +++ b/src/Maui/Bitwarden/Pages/Accounts/TwoFactorPage.xaml.cs @@ -0,0 +1,208 @@ +using System; +using System.Threading.Tasks; +using Bit.App.Controls; +using Bit.App.Models; +using Bit.App.Utilities; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Pages +{ + public partial class TwoFactorPage : BaseContentPage + { + private readonly IBroadcasterService _broadcasterService; + private readonly IMessagingService _messagingService; + private readonly AppOptions _appOptions; + + private TwoFactorPageViewModel _vm; + private bool _inited; + private bool _authingWithSso; + private string _orgIdentifier; + + public TwoFactorPage(bool? authingWithSso = false, AppOptions appOptions = null, string orgIdentifier = null) + { + InitializeComponent(); + SetActivityIndicator(); + _authingWithSso = authingWithSso ?? false; + _appOptions = appOptions; + _orgIdentifier = orgIdentifier; + _broadcasterService = ServiceContainer.Resolve("broadcasterService"); + _messagingService = ServiceContainer.Resolve("messagingService"); + _vm = BindingContext as TwoFactorPageViewModel; + _vm.Page = this; + _vm.StartSetPasswordAction = () => + Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync()); + _vm.TwoFactorAuthSuccessAction = () => + Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessAsync()); + _vm.UpdateTempPasswordAction = + () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync()); + _vm.CloseAction = async () => await Navigation.PopModalAsync(); + DuoWebView = _duoWebView; + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + if (Device.RuntimePlatform == Device.Android) + { + ToolbarItems.Remove(_cancelItem); + } + // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes + if (Device.RuntimePlatform == Device.iOS) + { + ToolbarItems.Add(_moreItem); + } + else + { + ToolbarItems.Add(_useAnotherTwoStepMethod); + } + } + + public HybridWebView DuoWebView { get; set; } + + protected async override void OnAppearing() + { + base.OnAppearing(); + _broadcasterService.Subscribe(nameof(TwoFactorPage), (message) => + { + if (message.Command == "gotYubiKeyOTP") + { + var token = (string)message.Data; + if (_vm.YubikeyMethod && !string.IsNullOrWhiteSpace(token) && + token.Length == 44 && !token.Contains(" ")) + { + Device.BeginInvokeOnMainThread(async () => + { + _vm.Token = token; + await _vm.SubmitAsync(); + }); + } + } + else if (message.Command == "resumeYubiKey") + { + if (_vm.YubikeyMethod) + { + _messagingService.Send("listenYubiKeyOTP", true); + } + } + }); + + await LoadOnAppearedAsync(_scrollView, true, () => + { + if (!_inited) + { + _inited = true; + _vm.Init(); + } + if (_vm.TotpMethod) + { + RequestFocus(_totpEntry); + } + else if (_vm.YubikeyMethod) + { + RequestFocus(_yubikeyTokenEntry); + } + return Task.FromResult(0); + }); + } + + protected override void OnDisappearing() + { + base.OnDisappearing(); + if (!_vm.YubikeyMethod) + { + _messagingService.Send("listenYubiKeyOTP", false); + _broadcasterService.Unsubscribe(nameof(TwoFactorPage)); + } + } + protected override bool OnBackButtonPressed() + { + if (_vm.YubikeyMethod) + { + _messagingService.Send("listenYubiKeyOTP", false); + _broadcasterService.Unsubscribe(nameof(TwoFactorPage)); + } + return base.OnBackButtonPressed(); + } + + private async void Continue_Clicked(object sender, EventArgs e) + { + if (DoOnce()) + { + await _vm.SubmitAsync(); + } + } + + private async void Methods_Clicked(object sender, EventArgs e) + { + if (DoOnce()) + { + await _vm.AnotherMethodAsync(); + } + } + + private async void ResendEmail_Clicked(object sender, EventArgs e) + { + if (DoOnce()) + { + await _vm.SendEmailAsync(true, true); + } + } + + private void Close_Clicked(object sender, System.EventArgs e) + { + if (DoOnce()) + { + _vm.CloseAction(); + } + } + + private async void TryAgain_Clicked(object sender, EventArgs e) + { + if (DoOnce()) + { + if (_vm.Fido2Method) + { + await _vm.Fido2AuthenticateAsync(); + } + else if (_vm.YubikeyMethod) + { + _messagingService.Send("listenYubiKeyOTP", true); + } + } + } + + private async Task StartSetPasswordAsync() + { + _vm.CloseAction(); + var page = new SetPasswordPage(_appOptions, _orgIdentifier); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + + private async Task UpdateTempPasswordAsync() + { + var page = new UpdateTempPasswordPage(); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + + private async Task TwoFactorAuthSuccessAsync() + { + if (_authingWithSso) + { + Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions)); + } + else + { + if (AppHelpers.SetAlternateMainPage(_appOptions)) + { + return; + } + var previousPage = await AppHelpers.ClearPreviousPage(); + Application.Current.MainPage = new TabsPage(_appOptions, previousPage); + } + } + + private void Token_TextChanged(object sender, TextChangedEventArgs e) + { + _vm.EnableContinue = !string.IsNullOrWhiteSpace(e.NewTextValue); + } + } +} diff --git a/src/Maui/Bitwarden/Pages/Accounts/TwoFactorPageViewModel.cs b/src/Maui/Bitwarden/Pages/Accounts/TwoFactorPageViewModel.cs new file mode 100644 index 000000000..a419d23dd --- /dev/null +++ b/src/Maui/Bitwarden/Pages/Accounts/TwoFactorPageViewModel.cs @@ -0,0 +1,428 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using System.Windows.Input; +using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.App.Utilities; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Request; +using Bit.Core.Utilities; +using Newtonsoft.Json; +using Bit.App.Utilities; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Pages +{ + public class TwoFactorPageViewModel : CaptchaProtectedViewModel + { + private readonly IDeviceActionService _deviceActionService; + private readonly IAuthService _authService; + private readonly ISyncService _syncService; + private readonly IApiService _apiService; + private readonly IPlatformUtilsService _platformUtilsService; + private readonly IEnvironmentService _environmentService; + private readonly IMessagingService _messagingService; + private readonly IBroadcasterService _broadcasterService; + private readonly IStateService _stateService; + private readonly II18nService _i18nService; + private readonly IAppIdService _appIdService; + private readonly ILogger _logger; + + private TwoFactorProviderType? _selectedProviderType; + private string _totpInstruction; + private string _webVaultUrl = "https://vault.bitwarden.com"; + private bool _authingWithSso = false; + private bool _enableContinue = false; + private bool _showContinue = true; + + public TwoFactorPageViewModel() + { + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _authService = ServiceContainer.Resolve("authService"); + _syncService = ServiceContainer.Resolve("syncService"); + _apiService = ServiceContainer.Resolve("apiService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _environmentService = ServiceContainer.Resolve("environmentService"); + _messagingService = ServiceContainer.Resolve("messagingService"); + _broadcasterService = ServiceContainer.Resolve("broadcasterService"); + _stateService = ServiceContainer.Resolve("stateService"); + _i18nService = ServiceContainer.Resolve("i18nService"); + _appIdService = ServiceContainer.Resolve("appIdService"); + _logger = ServiceContainer.Resolve(); + + PageTitle = AppResources.TwoStepLogin; + SubmitCommand = new Command(async () => await SubmitAsync()); + MoreCommand = new AsyncCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false); + } + + public string TotpInstruction + { + get => _totpInstruction; + set => SetProperty(ref _totpInstruction, value); + } + + public bool Remember { get; set; } + + public string Token { get; set; } + + public bool DuoMethod => SelectedProviderType == TwoFactorProviderType.Duo || + SelectedProviderType == TwoFactorProviderType.OrganizationDuo; + + public bool Fido2Method => SelectedProviderType == TwoFactorProviderType.Fido2WebAuthn; + + public bool YubikeyMethod => SelectedProviderType == TwoFactorProviderType.YubiKey; + + public bool AuthenticatorMethod => SelectedProviderType == TwoFactorProviderType.Authenticator; + + public bool EmailMethod => SelectedProviderType == TwoFactorProviderType.Email; + + public bool TotpMethod => AuthenticatorMethod || EmailMethod; + + public bool ShowTryAgain => (YubikeyMethod && // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes +Device.RuntimePlatform == Device.iOS) || Fido2Method; + + public bool ShowContinue + { + get => _showContinue; + set => SetProperty(ref _showContinue, value); + } + + public bool EnableContinue + { + get => _enableContinue; + set => SetProperty(ref _enableContinue, value); + } + + public string YubikeyInstruction => // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes +Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos : + AppResources.YubiKeyInstruction; + + public TwoFactorProviderType? SelectedProviderType + { + get => _selectedProviderType; + set => SetProperty(ref _selectedProviderType, value, additionalPropertyNames: new string[] + { + nameof(EmailMethod), + nameof(DuoMethod), + nameof(Fido2Method), + nameof(YubikeyMethod), + nameof(AuthenticatorMethod), + nameof(TotpMethod), + nameof(ShowTryAgain), + }); + } + public Command SubmitCommand { get; } + public ICommand MoreCommand { get; } + public Action TwoFactorAuthSuccessAction { get; set; } + public Action StartSetPasswordAction { get; set; } + public Action CloseAction { get; set; } + public Action UpdateTempPasswordAction { get; set; } + + protected override II18nService i18nService => _i18nService; + protected override IEnvironmentService environmentService => _environmentService; + protected override IDeviceActionService deviceActionService => _deviceActionService; + protected override IPlatformUtilsService platformUtilsService => _platformUtilsService; + + public void Init() + { + if ((!_authService.AuthingWithSso() && !_authService.AuthingWithPassword()) || + _authService.TwoFactorProvidersData == null) + { + // TODO: dismiss modal? + return; + } + + _authingWithSso = _authService.AuthingWithSso(); + + if (!string.IsNullOrWhiteSpace(_environmentService.BaseUrl)) + { + _webVaultUrl = _environmentService.BaseUrl; + } + else if (!string.IsNullOrWhiteSpace(_environmentService.WebVaultUrl)) + { + _webVaultUrl = _environmentService.WebVaultUrl; + } + + SelectedProviderType = _authService.GetDefaultTwoFactorProvider(_platformUtilsService.SupportsFido2()); + Load(); + } + + public void Load() + { + if (SelectedProviderType == null) + { + PageTitle = AppResources.LoginUnavailable; + return; + } + var page = Page as TwoFactorPage; + PageTitle = _authService.TwoFactorProviders[SelectedProviderType.Value].Name; + var providerData = _authService.TwoFactorProvidersData[SelectedProviderType.Value]; + switch (SelectedProviderType.Value) + { + case TwoFactorProviderType.Fido2WebAuthn: + Fido2AuthenticateAsync(providerData); + break; + case TwoFactorProviderType.YubiKey: + _messagingService.Send("listenYubiKeyOTP", true); + break; + case TwoFactorProviderType.Duo: + case TwoFactorProviderType.OrganizationDuo: + var host = WebUtility.UrlEncode(providerData["Host"] as string); + var req = WebUtility.UrlEncode(providerData["Signature"] as string); + page.DuoWebView.Uri = $"{_webVaultUrl}/duo-connector.html?host={host}&request={req}"; + page.DuoWebView.RegisterAction(sig => + { + Token = sig; + Device.BeginInvokeOnMainThread(async () => await SubmitAsync()); + }); + break; + case TwoFactorProviderType.Email: + TotpInstruction = string.Format(AppResources.EnterVerificationCodeEmail, + providerData["Email"] as string); + if (_authService.TwoFactorProvidersData.Count > 1) + { + var emailTask = Task.Run(() => SendEmailAsync(false, false)); + } + break; + case TwoFactorProviderType.Authenticator: + TotpInstruction = AppResources.EnterVerificationCodeApp; + break; + default: + break; + } + + if (!YubikeyMethod) + { + _messagingService.Send("listenYubiKeyOTP", false); + } + ShowContinue = !(SelectedProviderType == null || DuoMethod || Fido2Method); + } + + public async Task Fido2AuthenticateAsync(Dictionary providerData = null) + { + await _deviceActionService.ShowLoadingAsync(AppResources.Validating); + + if (providerData == null) + { + providerData = _authService.TwoFactorProvidersData[TwoFactorProviderType.Fido2WebAuthn]; + } + + var callbackUri = "bitwarden://webauthn-callback"; + var data = AppHelpers.EncodeDataParameter(new + { + callbackUri = callbackUri, + data = JsonConvert.SerializeObject(providerData), + headerText = AppResources.Fido2Title, + btnText = AppResources.Fido2AuthenticateWebAuthn, + btnReturnText = AppResources.Fido2ReturnToApp, + }); + + var url = _webVaultUrl + "/webauthn-mobile-connector.html?" + "data=" + data + + "&parent=" + Uri.EscapeDataString(callbackUri) + "&v=2"; + + WebAuthenticatorResult authResult = null; + try + { + var options = new WebAuthenticatorOptions + { + Url = new Uri(url), + CallbackUrl = new Uri(callbackUri), + PrefersEphemeralWebBrowserSession = true, + }; + authResult = await WebAuthenticator.AuthenticateAsync(options); + } + catch (TaskCanceledException) + { + // user canceled + await _deviceActionService.HideLoadingAsync(); + return; + } + + string response = null; + if (authResult != null && authResult.Properties.TryGetValue("data", out var resultData)) + { + response = Uri.UnescapeDataString(resultData); + } + if (!string.IsNullOrWhiteSpace(response)) + { + Token = response; + await SubmitAsync(false); + } + else + { + await _deviceActionService.HideLoadingAsync(); + if (authResult != null && authResult.Properties.TryGetValue("error", out var resultError)) + { + var message = AppResources.Fido2CheckBrowser + "\n\n" + resultError; + await _platformUtilsService.ShowDialogAsync(message, AppResources.AnErrorHasOccurred, + AppResources.Ok); + } + else + { + await _platformUtilsService.ShowDialogAsync(AppResources.Fido2CheckBrowser, + AppResources.AnErrorHasOccurred, AppResources.Ok); + } + } + } + + public async Task SubmitAsync(bool showLoading = true) + { + if (SelectedProviderType == null) + { + return; + } + if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None) + { + await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, + AppResources.InternetConnectionRequiredTitle, AppResources.Ok); + return; + } + if (string.IsNullOrWhiteSpace(Token)) + { + await _platformUtilsService.ShowDialogAsync( + string.Format(AppResources.ValidationFieldRequired, AppResources.VerificationCode), + AppResources.AnErrorHasOccurred, AppResources.Ok); + return; + } + if (SelectedProviderType == TwoFactorProviderType.Email || + SelectedProviderType == TwoFactorProviderType.Authenticator) + { + Token = Token.Replace(" ", string.Empty).Trim(); + } + + try + { + if (showLoading) + { + await _deviceActionService.ShowLoadingAsync(AppResources.Validating); + } + var result = await _authService.LogInTwoFactorAsync(SelectedProviderType.Value, Token, _captchaToken, Remember); + + if (result.CaptchaNeeded) + { + if (await HandleCaptchaAsync(result.CaptchaSiteKey)) + { + await SubmitAsync(false); + _captchaToken = null; + } + return; + } + _captchaToken = null; + + var task = Task.Run(() => _syncService.FullSyncAsync(true)); + await _deviceActionService.HideLoadingAsync(); + _messagingService.Send("listenYubiKeyOTP", false); + _broadcasterService.Unsubscribe(nameof(TwoFactorPage)); + + if (_authingWithSso && result.ResetMasterPassword) + { + StartSetPasswordAction?.Invoke(); + } + else if (result.ForcePasswordReset) + { + UpdateTempPasswordAction?.Invoke(); + } + else + { + TwoFactorAuthSuccessAction?.Invoke(); + } + } + catch (ApiException e) + { + _captchaToken = null; + await _deviceActionService.HideLoadingAsync(); + if (e?.Error != null) + { + await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(), + AppResources.AnErrorHasOccurred, AppResources.Ok); + } + } + } + + private async Task MoreAsync() + { + var selection = await _deviceActionService.DisplayActionSheetAsync(AppResources.Options, AppResources.Cancel, null, AppResources.UseAnotherTwoStepMethod); + if (selection == AppResources.UseAnotherTwoStepMethod) + { + await AnotherMethodAsync(); + } + } + + public async Task AnotherMethodAsync() + { + var supportedProviders = _authService.GetSupportedTwoFactorProviders(); + var options = supportedProviders.Select(p => p.Name).ToList(); + options.Add(AppResources.RecoveryCodeTitle); + var method = await _deviceActionService.DisplayActionSheetAsync(AppResources.TwoStepLoginOptions, + AppResources.Cancel, null, options.ToArray()); + if (method == AppResources.RecoveryCodeTitle) + { + _platformUtilsService.LaunchUri("https://bitwarden.com/help/lost-two-step-device/"); + } + else if (method != AppResources.Cancel && method != null) + { + var selected = supportedProviders.FirstOrDefault(p => p.Name == method)?.Type; + if (selected == SelectedProviderType) + { + // Nothing changed + return; + } + SelectedProviderType = selected; + Load(); + } + } + + public async Task SendEmailAsync(bool showLoading, bool doToast) + { + if (!EmailMethod) + { + return false; + } + if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None) + { + await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, + AppResources.InternetConnectionRequiredTitle, AppResources.Ok); + return false; + } + try + { + if (showLoading) + { + await _deviceActionService.ShowLoadingAsync(AppResources.Submitting); + } + var request = new TwoFactorEmailRequest + { + Email = _authService.Email, + MasterPasswordHash = _authService.MasterPasswordHash, + DeviceIdentifier = await _appIdService.GetAppIdAsync() + }; + await _apiService.PostTwoFactorEmailAsync(request); + if (showLoading) + { + await _deviceActionService.HideLoadingAsync(); + } + if (doToast) + { + _platformUtilsService.ShowToast("success", null, AppResources.VerificationEmailSent); + } + return true; + } + catch (ApiException) + { + if (showLoading) + { + await _deviceActionService.HideLoadingAsync(); + } + await _platformUtilsService.ShowDialogAsync(AppResources.VerificationEmailNotSent, + AppResources.AnErrorHasOccurred, AppResources.Ok); + return false; + } + } + } +} diff --git a/src/Maui/Bitwarden/Pages/Accounts/UpdateTempPasswordPage.xaml b/src/Maui/Bitwarden/Pages/Accounts/UpdateTempPasswordPage.xaml new file mode 100644 index 000000000..911033cad --- /dev/null +++ b/src/Maui/Bitwarden/Pages/Accounts/UpdateTempPasswordPage.xaml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Maui/Bitwarden/Pages/Accounts/UpdateTempPasswordPage.xaml.cs b/src/Maui/Bitwarden/Pages/Accounts/UpdateTempPasswordPage.xaml.cs new file mode 100644 index 000000000..2d23eda08 --- /dev/null +++ b/src/Maui/Bitwarden/Pages/Accounts/UpdateTempPasswordPage.xaml.cs @@ -0,0 +1,87 @@ +using System; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Pages +{ + public partial class UpdateTempPasswordPage : BaseContentPage + { + private readonly IMessagingService _messagingService; + private readonly IPlatformUtilsService _platformUtilsService; + private readonly UpdateTempPasswordPageViewModel _vm; + private readonly string _pageName; + + public UpdateTempPasswordPage() + { + // Service Init + _messagingService = ServiceContainer.Resolve("messagingService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + + // Binding + InitializeComponent(); + _pageName = string.Concat(nameof(UpdateTempPasswordPage), "_", DateTime.UtcNow.Ticks); + _vm = BindingContext as UpdateTempPasswordPageViewModel; + _vm.Page = this; + SetActivityIndicator(); + + // Actions Declaration + _vm.LogOutAction = () => + { + _messagingService.Send("logout"); + }; + _vm.UpdateTempPasswordSuccessAction = () => Device.BeginInvokeOnMainThread(UpdateTempPasswordSuccess); + + // Link fields that will be referenced in codebehind + MasterPasswordEntry = _masterPassword; + ConfirmMasterPasswordEntry = _confirmMasterPassword; + + // Return Types and Commands + _masterPassword.ReturnType = ReturnType.Next; + _masterPassword.ReturnCommand = new Command(() => _confirmMasterPassword.Focus()); + _confirmMasterPassword.ReturnType = ReturnType.Next; + _confirmMasterPassword.ReturnCommand = new Command(() => _hint.Focus()); + } + + public Entry MasterPasswordEntry { get; set; } + public Entry ConfirmMasterPasswordEntry { get; set; } + + protected override async void OnAppearing() + { + base.OnAppearing(); + await LoadOnAppearedAsync(_mainLayout, true, async () => + { + await _vm.InitAsync(true); + }); + RequestFocus(_masterPassword); + } + + private async void Submit_Clicked(object sender, EventArgs e) + { + if (DoOnce()) + { + await _vm.SubmitAsync(); + } + } + + private async void LogOut_Clicked(object sender, EventArgs e) + { + if (DoOnce()) + { + var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.LogoutConfirmation, + AppResources.LogOut, AppResources.Yes, AppResources.Cancel); + if (confirmed) + { + _vm.LogOutAction(); + } + } + } + + private void UpdateTempPasswordSuccess() + { + _messagingService.Send("logout"); + } + } +} diff --git a/src/Maui/Bitwarden/Pages/Accounts/UpdateTempPasswordPageViewModel.cs b/src/Maui/Bitwarden/Pages/Accounts/UpdateTempPasswordPageViewModel.cs new file mode 100644 index 000000000..c8dd882d6 --- /dev/null +++ b/src/Maui/Bitwarden/Pages/Accounts/UpdateTempPasswordPageViewModel.cs @@ -0,0 +1,172 @@ +using System; +using System.Threading.Tasks; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; +using Bit.Core.Utilities; +using Bit.App.Utilities; +using Microsoft.Maui.Controls; +using Microsoft.Maui; + +namespace Bit.App.Pages +{ + public class UpdateTempPasswordPageViewModel : BaseChangePasswordViewModel + { + private readonly IUserVerificationService _userVerificationService; + + private ForcePasswordResetReason _reason = ForcePasswordResetReason.AdminForcePasswordReset; + + public UpdateTempPasswordPageViewModel() + { + PageTitle = AppResources.UpdateMasterPassword; + TogglePasswordCommand = new Command(TogglePassword); + ToggleConfirmPasswordCommand = new Command(ToggleConfirmPassword); + SubmitCommand = new AsyncCommand(SubmitAsync, + onException: ex => HandleException(ex), + allowsMultipleExecutions: false); + + _userVerificationService = ServiceContainer.Resolve(); + } + + public AsyncCommand SubmitCommand { get; } + public Command TogglePasswordCommand { get; } + public Command ToggleConfirmPasswordCommand { get; } + public Action UpdateTempPasswordSuccessAction { get; set; } + public Action LogOutAction { get; set; } + public string CurrentMasterPassword { get; set; } + + public override async Task InitAsync(bool forceSync = false) + { + await base.InitAsync(forceSync); + + var forcePasswordResetReason = await _stateService.GetForcePasswordResetReasonAsync(); + + if (forcePasswordResetReason.HasValue) + { + _reason = forcePasswordResetReason.Value; + } + } + + public bool RequireCurrentPassword + { + get => _reason == ForcePasswordResetReason.WeakMasterPasswordOnLogin; + } + + public string UpdateMasterPasswordWarningText + { + get + { + return _reason == ForcePasswordResetReason.WeakMasterPasswordOnLogin + ? AppResources.UpdateWeakMasterPasswordWarning + : AppResources.UpdateMasterPasswordWarning; + } + } + + public void TogglePassword() + { + ShowPassword = !ShowPassword; + (Page as UpdateTempPasswordPage).MasterPasswordEntry.Focus(); + } + + public void ToggleConfirmPassword() + { + ShowPassword = !ShowPassword; + (Page as UpdateTempPasswordPage).ConfirmMasterPasswordEntry.Focus(); + } + + public async Task SubmitAsync() + { + if (!await ValidateMasterPasswordAsync()) + { + return; + } + + if (RequireCurrentPassword && + !await _userVerificationService.VerifyUser(CurrentMasterPassword, VerificationType.MasterPassword)) + { + return; + } + + // Retrieve details for key generation + var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile)); + var email = await _stateService.GetEmailAsync(); + + // Create new key and hash new password + var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdfConfig); + var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key); + + // Create new encKey for the User + var newEncKey = await _cryptoService.RemakeEncKeyAsync(key); + + // Initiate API action + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.UpdatingPassword); + + switch (_reason) + { + case ForcePasswordResetReason.AdminForcePasswordReset: + await UpdateTempPasswordAsync(masterPasswordHash, newEncKey.Item2.EncryptedString); + break; + case ForcePasswordResetReason.WeakMasterPasswordOnLogin: + await UpdatePasswordAsync(masterPasswordHash, newEncKey.Item2.EncryptedString); + break; + default: + throw new ArgumentOutOfRangeException(); + } + await _deviceActionService.HideLoadingAsync(); + + // Clear the force reset password reason + await _stateService.SetForcePasswordResetReasonAsync(null); + + _platformUtilsService.ShowToast(null, null, AppResources.UpdatedMasterPassword); + + UpdateTempPasswordSuccessAction?.Invoke(); + } + catch (ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + if (e?.Error != null) + { + await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(), + AppResources.AnErrorHasOccurred, AppResources.Ok); + } + else + { + await _platformUtilsService.ShowDialogAsync(AppResources.UpdatePasswordError, + AppResources.AnErrorHasOccurred, AppResources.Ok); + } + } + } + + private async Task UpdateTempPasswordAsync(string newMasterPasswordHash, string newEncKey) + { + var request = new UpdateTempPasswordRequest + { + Key = newEncKey, + NewMasterPasswordHash = newMasterPasswordHash, + MasterPasswordHint = Hint + }; + + await _apiService.PutUpdateTempPasswordAsync(request); + } + + private async Task UpdatePasswordAsync(string newMasterPasswordHash, string newEncKey) + { + var currentPasswordHash = await _cryptoService.HashPasswordAsync(CurrentMasterPassword, null); + + var request = new PasswordRequest + { + MasterPasswordHash = currentPasswordHash, + Key = newEncKey, + NewMasterPasswordHash = newMasterPasswordHash, + MasterPasswordHint = Hint + }; + + await _apiService.PostPasswordAsync(request); + } + } +} diff --git a/src/Maui/Bitwarden/Pages/Accounts/VerificationCodePage.xaml b/src/Maui/Bitwarden/Pages/Accounts/VerificationCodePage.xaml new file mode 100644 index 000000000..0f143faab --- /dev/null +++ b/src/Maui/Bitwarden/Pages/Accounts/VerificationCodePage.xaml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + +
+ +