From fa6bac3b4337768101980ce04bf0d2ccf2f8ca14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lison=20Fernandes?= Date: Wed, 7 Dec 2022 16:39:20 +0000 Subject: [PATCH] EC-395 Apple Watch MVP (#2228) * [EC-426] Add watchOS PoC app (#2054) * EC-426 Added watchOS app, configured iOS.csproj to bundle the output of XCode build into the Xamarin iOS app and added some custom logic to use WCSession to communicate between the iOS and the watchOS apps * EC-426 Removed Info.plist from iOS.Core project given that it's not needed * [EC-426] Added new encrypted watch app profiles * EC-426 added configuration for building watchApp and bundle it up on the iOS one * EC-426 Fix build for watchOS * EC-426 Fix build for watchOS applied shell bash * EC-426 Fix build for watchOS echo * EC-426 Fix build for watchOS simplify * EC-426 Fix build for watchOS added workspace path * EC-426 Changed code sign identity of watchOS project to Apple Distribution * EC-426 added manual code sign style and specified the provisioning profile for the targets on the watch xcode project * EC-426 updated path to watchOS on release on iOS.csproj and disabled android and f-.droid * EC-426 fix build * EC-426 fix path and check listing of directory of watchOS output just in case * EC-426 Fix Apple Watch build to list the folder recursively just in case we need to change the path for the watch bundle * EC-426 TEMP Change texts on input on login and lock to show that the app is for the Watch PoC testing * EC-426 Fix WatchApp build path * EC-426 Added WatchOS AppIcons * EC-426 added gitignore for XCode project removed files supposed to be ignored * EC-426 Cleaned the code a bit to avoid misbehavior * EC-426 Code cleanup Co-authored-by: Joseph Flinn * [EC-585] Added data, encryption and some helpers and structure to the Watch app (#2164) * [EC-585] Added foundation classes on the watch to handle CoreData and some fixes on the communication of the ciphers, also some helper classes to store in keychain and encrypt data * EC-585 Added keychain helper, encryption helpers and added data storage using CoreData configuring it appropiately. View and ViewModel are here only to test that the fetching/saving works but it's not the actual UI of the watch app. Also removed all the places where the automatic file signature was added by XCode * EC-585 Fixed CipherServiceMock to implement protocol * EC-585 Fixed DeviceActionService duplicated services * [EC-614] Apple Watch MVP Cipher list UI (#2175) * [EC-585] Added foundation classes on the watch to handle CoreData and some fixes on the communication of the ciphers, also some helper classes to store in keychain and encrypt data * EC-585 Added keychain helper, encryption helpers and added data storage using CoreData configuring it appropiately. View and ViewModel are here only to test that the fetching/saving works but it's not the actual UI of the watch app. Also removed all the places where the automatic file signature was added by XCode * EC-585 Fixed CipherServiceMock to implement protocol * EC-585 Fixed DeviceActionService duplicated services * EC-614 Implemented watch ciphers list UI * [EC-615] Apple Watch MVP Cipher details UI (#2192) * [EC-585] Added foundation classes on the watch to handle CoreData and some fixes on the communication of the ciphers, also some helper classes to store in keychain and encrypt data * EC-585 Added keychain helper, encryption helpers and added data storage using CoreData configuring it appropiately. View and ViewModel are here only to test that the fetching/saving works but it's not the actual UI of the watch app. Also removed all the places where the automatic file signature was added by XCode * EC-585 Fixed CipherServiceMock to implement protocol * EC-585 Fixed DeviceActionService duplicated services * EC-614 Implemented watch ciphers list UI * EC-615 Added cipher details UI to watch and also implemented logic and helpers to generate the TOTPs * EC-615 Added value transformer to login uris on the cipher entity * EC-617 Added state view on watch app and some state helpers and wired it on the CipherListView. Also added some images (#2195) * [EC-581] Implement Apple Watch MVP Sync (#2206) * EC-581 Implemented sync iPhone -> watchOS, fix some issues with the watch database and sync flows for login/locks/multiple accounts * EC-581 Added watch sync on unlocking and need setup state when no user is synced and the session is not active * EC-581 Removed unused method * EC-581 Fix format * EC-759 Added avatar row on cipher list header to display avatar icon and email (#2213) * [EC-786] Apple Watch MVP Sync fixes (#2214) * EC-786 Commented things that are not going to be included on the MVP and fixed issue on the dictionary sent on the applicationContext to have a changing key based on time * EC-786 Commented need unlock state * EC-579 Added logic for Connect To Watch on iOS settings and moved it to the correct place. Also improved the synchronization and watch session activation logic (#2218) * EC-616 Added search header for ciphers and polished the code (#2226) Co-authored-by: Federico Maccaroni Co-authored-by: Joseph Flinn --- .../resources/export-options-app-store.plist | 4 + .../dist_watch_app.mobileprovision.gpg | Bin 0 -> 7789 bytes ...st_watch_app_extension.mobileprovision.gpg | Bin 0 -> 7811 bytes .github/workflows/build.yml | 35 + .gitignore | 125 +++ src/Android/Android.csproj | 1 + src/Android/MainApplication.cs | 12 +- src/Android/Services/DeviceActionService.cs | 1 + src/Android/Services/WatchDeviceService.cs | 29 + src/App/Abstractions/IDeviceActionService.cs | 1 + src/App/Pages/Accounts/LockPageViewModel.cs | 3 + .../SettingsPage/SettingsPageViewModel.cs | 46 +- .../Pages/Vault/CipherAddEditPageViewModel.cs | 4 + .../Pages/Vault/CipherDetailsPageViewModel.cs | 5 + src/App/Resources/AppResources.Designer.cs | 9 + src/App/Resources/AppResources.resx | 3 + src/App/Services/BaseWatchDeviceService.cs | 128 +++ .../AccountManagement/AccountsManager.cs | 8 +- src/Core/Abstractions/ICipherService.cs | 2 +- src/Core/Abstractions/IStateService.cs | 3 + src/Core/Abstractions/IWatchDeviceService.cs | 12 + src/Core/Constants.cs | 1 + src/Core/Enums/WatchState.cs | 13 + src/Core/Models/View/SimpleCipherView.cs | 48 + src/Core/Models/View/WatchDTO.cs | 44 + src/Core/Services/AuthService.cs | 3 + src/Core/Services/CipherService.cs | 17 +- src/Core/Services/StateService.cs | 26 +- src/Core/Services/SyncService.cs | 4 + src/iOS.Core/Services/WatchDeviceService.cs | 48 + .../Utilities/DictionaryExtensions.cs | 26 + src/iOS.Core/Utilities/WCSessionManager.cs | 185 ++++ src/iOS.Core/Utilities/iOSCoreHelpers.cs | 9 +- src/iOS.Core/iOS.Core.csproj | 3 + src/iOS/AppDelegate.cs | 20 +- src/iOS/iOS.csproj | 22 + .../AccentColor.colorset/Contents.json | 20 + .../AppIcon.appiconset/100.png | Bin 0 -> 3995 bytes .../AppIcon.appiconset/102.png | Bin 0 -> 3451 bytes .../AppIcon.appiconset/172.png | Bin 0 -> 6559 bytes .../AppIcon.appiconset/196.png | Bin 0 -> 7371 bytes .../AppIcon.appiconset/216.png | Bin 0 -> 8314 bytes .../AppIcon.appiconset/234.png | Bin 0 -> 6753 bytes .../Assets.xcassets/AppIcon.appiconset/48.png | Bin 0 -> 2043 bytes .../Assets.xcassets/AppIcon.appiconset/55.png | Bin 0 -> 2443 bytes .../Assets.xcassets/AppIcon.appiconset/66.png | Bin 0 -> 2393 bytes .../Assets.xcassets/AppIcon.appiconset/88.png | Bin 0 -> 3673 bytes .../Assets.xcassets/AppIcon.appiconset/92.png | Bin 0 -> 3128 bytes .../AppIcon.appiconset/Contents.json | 138 +++ .../AppIcon.appiconset/Icon-1024.png | Bin 0 -> 17162 bytes .../AppIcon.appiconset/Icon-59.png | Bin 0 -> 1157 bytes .../AppIcon.appiconset/Icon-80.png | Bin 0 -> 1419 bytes .../AppIcon.appiconset/Icon-87.png | Bin 0 -> 1550 bytes .../Assets.xcassets/Contents.json | 6 + .../BitwardenImagetype.imageset/Contents.json | 23 + .../logo-horizontal-blue (2) 1.pdf | Bin 0 -> 20349 bytes .../logo-horizontal-blue (2) 2.pdf | Bin 0 -> 20349 bytes .../logo-horizontal-blue (2) 3.pdf | Bin 0 -> 20349 bytes .../Circular.imageset/Contents.json | 25 + .../Contents.json | 53 + .../Extra Large.imageset/Contents.json | 25 + .../Graphic Bezel.imageset/Contents.json | 20 + .../Graphic Circular.imageset/Contents.json | 20 + .../Graphic Corner.imageset/Contents.json | 20 + .../Contents.json | 25 + .../Contents.json | 20 + .../Modular.imageset/Contents.json | 25 + .../Utilitarian.imageset/Contents.json | 25 + .../Assets.xcassets/Contents.json | 6 + .../DarkTextMuted.colorset/Contents.json | 38 + .../DefaultCipherIcon.imageset/Contents.json | 23 + .../DefaultCipherIcon.imageset/globe 1.svg | 3 + .../DefaultCipherIcon.imageset/globe 2.svg | 3 + .../DefaultCipherIcon.imageset/globe.svg | 3 + .../Contents.json | 23 + .../emptystatedark 1.svg | 22 + .../emptystatedark 2.svg | 22 + .../emptystatedark.svg | 22 + .../ItemBackground.colorset/Contents.json | 38 + .../ComplicationController.swift | 52 + .../Controls/AvatarView.swift | 85 ++ .../Controls/CircularProgressView.swift | 39 + .../Controls/ImageView.swift | 112 ++ .../TrackableWithHeaderListView.swift | 40 + .../DataStorage/CoreDataHelper.swift | 127 +++ .../DataStorage/DBHelperProtocol.swift | 20 + .../BitwardenDB.xcdatamodel/contents | 17 + .../Entities/CipherEntity+CoreDataClass.swift | 43 + .../CipherEntity+CoreDataProperties.swift | 34 + .../StringEncryptionTransformer.swift | 48 + .../Helpers/IconImageHelper.swift | 38 + .../Helpers/KeychainHelper.swift | 94 ++ .../Helpers/LoggerHelper.swift | 54 + .../bitwarden WatchKit Extension/Info.plist | 16 + .../Localization/en.lproj/Localizable.strings | 9 + .../Models/Cipher.swift | 36 + .../Models/Mocks/CipherMock.swift | 17 + .../Models/User.swift | 7 + .../Models/VaultTimeoutAction.swift | 6 + .../Models/WatchDTO.swift | 26 + .../NotificationController.swift | 26 + .../NotificationView.swift | 13 + .../Preview Assets.xcassets/Contents.json | 6 + .../PushNotificationPayload.apns | 20 + .../Services/CipherService.swift | 70 ++ .../Services/CryptoFunctionService.swift | 25 + .../Services/CryptoService.swift | 97 ++ .../Services/EnvironmentService.swift | 45 + .../Services/Mocks/CipherServiceMock.swift | 26 + .../Services/StateService.swift | 73 ++ .../Services/TotpService.swift | 133 +++ .../Utilities/BWState.swift | 15 + .../Utilities/Base32.swift | 58 + .../Utilities/ColorUtils.swift | 60 ++ .../Utilities/DateExtensions.swift | 8 + .../Utilities/EmptyStateViewModifier.swift | 23 + .../Utilities/ErrorExtensions.swift | 69 ++ .../Utilities/JsonDecoderExtensions.swift | 40 + .../Utilities/StringExtensions.swift | 28 + .../Utilities/UInt64Extensions.swift | 9 + .../Utilities/URLExtensions.swift | 28 + .../Utilities/ViewExtensions.swift | 15 + .../ViewModels/BWStateViewModel.swift | 26 + .../ViewModels/CipherDetailsViewModel.swift | 63 ++ .../ViewModels/CipherListViewModel.swift | 95 ++ .../Views/BWStateView.swift | 37 + .../Views/CipherDetailsView.swift | 101 ++ .../Views/CipherItemView.swift | 57 + .../Views/CipherListView.swift | 134 +++ .../WatchConnectivityManager.swift | 116 ++ .../bitwardenApp.swift | 14 + .../bitwarden.xcodeproj/project.pbxproj | 987 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + ...arden WatchKit App (Complication).xcscheme | 94 ++ ...arden WatchKit App (Notification).xcscheme | 96 ++ .../xcschemes/bitwarden WatchKit App.xcscheme | 94 ++ .../xcshareddata/xcschemes/bitwarden.xcscheme | 78 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 ++ .../bitwarden/Assets.xcassets/Contents.json | 6 + .../bitwarden/bitwarden/ContentView.swift | 27 + .../Preview Assets.xcassets/Contents.json | 6 + .../bitwarden/WatchConnectivityManager.swift | 126 +++ .../bitwarden/bitwarden/bitwardenApp.swift | 10 + 145 files changed, 5625 insertions(+), 21 deletions(-) create mode 100644 .github/secrets/dist_watch_app.mobileprovision.gpg create mode 100644 .github/secrets/dist_watch_app_extension.mobileprovision.gpg create mode 100644 src/Android/Services/WatchDeviceService.cs create mode 100644 src/App/Services/BaseWatchDeviceService.cs create mode 100644 src/Core/Abstractions/IWatchDeviceService.cs create mode 100644 src/Core/Enums/WatchState.cs create mode 100644 src/Core/Models/View/SimpleCipherView.cs create mode 100644 src/Core/Models/View/WatchDTO.cs create mode 100644 src/iOS.Core/Services/WatchDeviceService.cs create mode 100644 src/iOS.Core/Utilities/DictionaryExtensions.cs create mode 100644 src/iOS.Core/Utilities/WCSessionManager.cs create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/100.png create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/102.png create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/172.png create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/196.png create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/216.png create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/234.png create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/48.png create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/55.png create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/66.png create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/88.png create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/92.png create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-1024.png create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-59.png create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-80.png create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-87.png create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/BitwardenImagetype.imageset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/BitwardenImagetype.imageset/logo-horizontal-blue (2) 1.pdf create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/BitwardenImagetype.imageset/logo-horizontal-blue (2) 2.pdf create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/BitwardenImagetype.imageset/logo-horizontal-blue (2) 3.pdf create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DarkTextMuted.colorset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe 1.svg create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe 2.svg create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe.svg create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark 1.svg create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark 2.svg create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark.svg create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/ItemBackground.colorset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/ComplicationController.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/AvatarView.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/CircularProgressView.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/ImageView.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/TrackableWithHeaderListView.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/DataStorage/CoreDataHelper.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/DataStorage/DBHelperProtocol.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/BitwardenDB.xcdatamodeld/BitwardenDB.xcdatamodel/contents create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/CipherEntity+CoreDataClass.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/CipherEntity+CoreDataProperties.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/StringEncryptionTransformer.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/IconImageHelper.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/KeychainHelper.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/LoggerHelper.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Info.plist create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/en.lproj/Localizable.strings create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/Cipher.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/Mocks/CipherMock.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/User.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/VaultTimeoutAction.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/WatchDTO.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/NotificationController.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/NotificationView.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/PushNotificationPayload.apns create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CipherService.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CryptoFunctionService.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CryptoService.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/EnvironmentService.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/Mocks/CipherServiceMock.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/StateService.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/TotpService.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/BWState.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/Base32.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ColorUtils.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/DateExtensions.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/EmptyStateViewModifier.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ErrorExtensions.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/JsonDecoderExtensions.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/StringExtensions.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/UInt64Extensions.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/URLExtensions.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ViewExtensions.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/ViewModels/BWStateViewModel.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/ViewModels/CipherDetailsViewModel.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/ViewModels/CipherListViewModel.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/BWStateView.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherDetailsView.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherItemView.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherListView.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/WatchConnectivityManager.swift create mode 100644 src/watchOS/bitwarden/bitwarden WatchKit Extension/bitwardenApp.swift create mode 100644 src/watchOS/bitwarden/bitwarden.xcodeproj/project.pbxproj create mode 100644 src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App (Complication).xcscheme create mode 100644 src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App (Notification).xcscheme create mode 100644 src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App.xcscheme create mode 100644 src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden.xcscheme create mode 100644 src/watchOS/bitwarden/bitwarden/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden/Assets.xcassets/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden/ContentView.swift create mode 100644 src/watchOS/bitwarden/bitwarden/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 src/watchOS/bitwarden/bitwarden/WatchConnectivityManager.swift create mode 100644 src/watchOS/bitwarden/bitwarden/bitwardenApp.swift diff --git a/.github/resources/export-options-app-store.plist b/.github/resources/export-options-app-store.plist index df3ed635b..dc991f2a8 100644 --- a/.github/resources/export-options-app-store.plist +++ b/.github/resources/export-options-app-store.plist @@ -14,6 +14,10 @@ Dist: Extension 2021 com.8bit.bitwarden.share-extension Dist: Share Extension 2021 + com.8bit.bitwarden.watchkitapp + Dist: Bitwarden Watch App + com.8bit.bitwarden.watchkitapp.watchkitextension + Dist: Bitwarden Watch App Extension diff --git a/.github/secrets/dist_watch_app.mobileprovision.gpg b/.github/secrets/dist_watch_app.mobileprovision.gpg new file mode 100644 index 0000000000000000000000000000000000000000..8981acabe5f6cd61aefd779ee8a5ce4b6b781a32 GIT binary patch literal 7789 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\/key>\s*1<\/string>/CFBundleVersion<\/key>\n\t'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.Extension/Info.plist perl -0777 -pi.bak -e 's/CFBundleVersion<\/key>\s*1<\/string>/CFBundleVersion<\/key>\n\t'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.Autofill/Info.plist perl -0777 -pi.bak -e 's/CFBundleVersion<\/key>\s*1<\/string>/CFBundleVersion<\/key>\n\t'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.ShareExtension/Info.plist + cd src/watchOS/bitwarden + agvtool new-version -all $BUILD_NUMBER + cd ../../.. shell: bash - name: Update Entitlements @@ -545,6 +556,8 @@ jobs: BITWARDEN_PROFILE_PATH=$HOME/secrets/dist_bitwarden.mobileprovision EXTENSION_PROFILE_PATH=$HOME/secrets/dist_extension.mobileprovision SHARE_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_share_extension.mobileprovision + WATCH_APP_PROFILE_PATH=$HOME/secrets/dist_watch_app.mobileprovision + WATCH_APP_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_watch_app_extension.mobileprovision PROFILES_DIR_PATH=$HOME/Library/MobileDevice/Provisioning\ Profiles mkdir -p "$PROFILES_DIR_PATH" @@ -560,6 +573,28 @@ jobs: SHARE_EXTENSION_UUID=$(grep UUID -A1 -a $SHARE_EXTENSION_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}") cp $SHARE_EXTENSION_PROFILE_PATH "$PROFILES_DIR_PATH/$SHARE_EXTENSION_UUID.mobileprovision" + + WATCH_APP_UUID=$(grep UUID -A1 -a $WATCH_APP_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}") + cp $WATCH_APP_PROFILE_PATH "$PROFILES_DIR_PATH/$WATCH_APP_UUID.mobileprovision" + + WATCH_APP_EXTENSION_UUID=$(grep UUID -A1 -a $WATCH_APP_EXTENSION_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}") + cp $WATCH_APP_EXTENSION_PROFILE_PATH "$PROFILES_DIR_PATH/$WATCH_APP_EXTENSION_UUID.mobileprovision" + shell: bash + + - name: Bulid WatchApp + run: | + echo "########################################" + echo "##### Build WatchApp with Release Configuration" + echo "########################################" + + xcodebuild archive -workspace ./src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace -configuration Release -scheme bitwarden\ WatchKit\ App -archivePath ./src/watchOS/bitwarden + + echo "########################################" + echo "##### Done" + echo "########################################" + cd src/watchOS + ls -R + cd ../.. shell: bash - name: Restore packages diff --git a/.gitignore b/.gitignore index 67fa6064c..107fad1df 100644 --- a/.gitignore +++ b/.gitignore @@ -210,3 +210,128 @@ project.lock.json .DS_Store src/App/Css tools + +# Created by https://www.toptal.com/developers/gitignore/api/swift,objective-c +# Edit at https://www.toptal.com/developers/gitignore?templates=swift,objective-c + +### Objective-C ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +# CocoaPods +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# Pods/ +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# fastlane +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +### Objective-C Patch ### + +### Swift ### +# Xcode +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + + + + + + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# Pods/ +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + + +# Code Injection +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + + +# End of https://www.toptal.com/developers/gitignore/api/swift,objective-c diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index 73a19d10e..8d01e1091 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -159,6 +159,7 @@ + diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index 8bca7cafa..56aafe4e5 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -45,9 +45,16 @@ namespace Bit.Droid if (ServiceContainer.RegisteredServices.Count == 0) { RegisterLocalServices(); + var deviceActionService = ServiceContainer.Resolve("deviceActionService"); ServiceContainer.Init(deviceActionService.DeviceUserAgent, Core.Constants.ClearCiphersCacheKey, Core.Constants.AndroidAllClearCipherCacheKeys); + + ServiceContainer.Register(new WatchDeviceService(ServiceContainer.Resolve(), + ServiceContainer.Resolve(), + ServiceContainer.Resolve(), + ServiceContainer.Resolve())); + InitializeAppSetup(); // TODO: Update when https://github.com/bitwarden/mobile/pull/1662 gets merged @@ -73,8 +80,9 @@ namespace Bit.Droid ServiceContainer.Resolve("platformUtilsService"), ServiceContainer.Resolve("authService"), ServiceContainer.Resolve("logger"), - ServiceContainer.Resolve("messagingService")); - ServiceContainer.Register("accountsManager", accountsManager); + ServiceContainer.Resolve("messagingService"), + ServiceContainer.Resolve()); + ServiceContainer.Register("accountsManager", accountsManager); } #if !FDROID if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat) diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs index f189c77ce..82d6f9fd8 100644 --- a/src/Android/Services/DeviceActionService.cs +++ b/src/Android/Services/DeviceActionService.cs @@ -18,6 +18,7 @@ using Bit.Core.Enums; using Bit.Core.Utilities; using Bit.Droid.Utilities; using Plugin.CurrentActivity; +using static Bit.App.Pages.SettingsPageViewModel; namespace Bit.Droid.Services { diff --git a/src/Android/Services/WatchDeviceService.cs b/src/Android/Services/WatchDeviceService.cs new file mode 100644 index 000000000..0cad001b3 --- /dev/null +++ b/src/Android/Services/WatchDeviceService.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; +using Bit.App.Services; +using Bit.Core.Abstractions; +using Bit.Core.Models; + +namespace Bit.Droid.Services +{ + public class WatchDeviceService : BaseWatchDeviceService + { + public WatchDeviceService(ICipherService cipherService, + IEnvironmentService environmentService, + IStateService stateService, + IVaultTimeoutService vaultTimeoutService) + : base(cipherService, environmentService, stateService, vaultTimeoutService) + { + } + + protected override bool IsSupported => false; + + public override bool IsConnected => false; + + protected override bool CanSendData => false; + + protected override Task SendDataToWatchAsync(WatchDTO watchDto) => throw new NotImplementedException(); + + protected override void ConnectToWatch() => throw new NotImplementedException(); + } +} diff --git a/src/App/Abstractions/IDeviceActionService.cs b/src/App/Abstractions/IDeviceActionService.cs index 70384100e..e539b156f 100644 --- a/src/App/Abstractions/IDeviceActionService.cs +++ b/src/App/Abstractions/IDeviceActionService.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Bit.Core.Enums; +using Bit.Core.Models; namespace Bit.App.Abstractions { diff --git a/src/App/Pages/Accounts/LockPageViewModel.cs b/src/App/Pages/Accounts/LockPageViewModel.cs index 5419916e9..5d2fccfd3 100644 --- a/src/App/Pages/Accounts/LockPageViewModel.cs +++ b/src/App/Pages/Accounts/LockPageViewModel.cs @@ -28,6 +28,7 @@ namespace Bit.App.Pages private readonly IBiometricService _biometricService; private readonly IKeyConnectorService _keyConnectorService; private readonly ILogger _logger; + private readonly IWatchDeviceService _watchDeviceService; private readonly WeakEventManager _secretEntryFocusWeakEventManager = new WeakEventManager(); private string _email; @@ -56,6 +57,7 @@ namespace Bit.App.Pages _biometricService = ServiceContainer.Resolve("biometricService"); _keyConnectorService = ServiceContainer.Resolve("keyConnectorService"); _logger = ServiceContainer.Resolve("logger"); + _watchDeviceService = ServiceContainer.Resolve(); PageTitle = AppResources.VerifyMasterPassword; TogglePasswordCommand = new Command(TogglePassword); @@ -387,6 +389,7 @@ namespace Bit.App.Pages private async Task DoContinueAsync() { await _stateService.SetBiometricLockedAsync(false); + _watchDeviceService.SyncDataToWatchAsync().FireAndForget(); _messagingService.Send("unlocked"); UnlockedAction?.Invoke(); } diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs index f586e6e79..bf00f5a3e 100644 --- a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs +++ b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs @@ -7,7 +7,10 @@ using Bit.App.Pages.Accounts; using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Enums; +using Bit.Core.Models; using Bit.Core.Models.Domain; +using Bit.Core.Models.View; +using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.Forms; @@ -32,6 +35,7 @@ namespace Bit.App.Pages private readonly IClipboardService _clipboardService; private readonly ILogger _loggerService; private readonly IPushNotificationService _pushNotificationService; + private readonly IWatchDeviceService _watchDeviceService; private const int CustomVaultTimeoutValue = -100; private bool _supportsBiometric; @@ -44,6 +48,7 @@ namespace Bit.App.Pages private bool _showChangeMasterPassword; private bool _reportLoggingEnabled; private bool _approvePasswordlessLoginRequests; + private bool _shouldConnectToWatch; private List> _vaultTimeouts = new List> @@ -87,6 +92,7 @@ namespace Bit.App.Pages _clipboardService = ServiceContainer.Resolve("clipboardService"); _loggerService = ServiceContainer.Resolve("logger"); _pushNotificationService = ServiceContainer.Resolve(); + _watchDeviceService = ServiceContainer.Resolve(); GroupedItems = new ObservableRangeCollection(); PageTitle = AppResources.Settings; @@ -138,6 +144,9 @@ namespace Bit.App.Pages !await _keyConnectorService.GetUsesKeyConnector(); _reportLoggingEnabled = await _loggerService.IsEnabled(); _approvePasswordlessLoginRequests = await _stateService.GetApprovePasswordlessLoginsAsync(); + + _shouldConnectToWatch = await _stateService.GetShouldConnectToWatchAsync(); + BuildList(); } @@ -601,19 +610,26 @@ namespace Bit.App.Pages ExecuteAsync = () => SetScreenCaptureAllowedAsync() }); } - var accountItems = new List + var accountItems = new List(); + if (Device.RuntimePlatform == Device.iOS) { - new SettingsPageListItem + accountItems.Add(new SettingsPageListItem { - Name = AppResources.FingerprintPhrase, - ExecuteAsync = () => FingerprintAsync() - }, - new SettingsPageListItem - { - Name = AppResources.LogOut, - ExecuteAsync = () => LogOutAsync() - } - }; + Name = AppResources.ConnectToWatch, + SubLabel = _shouldConnectToWatch ? AppResources.On : AppResources.Off, + ExecuteAsync = () => ToggleWatchConnectionAsync() + }); + } + accountItems.Add(new SettingsPageListItem + { + Name = AppResources.FingerprintPhrase, + ExecuteAsync = () => FingerprintAsync() + }); + accountItems.Add(new SettingsPageListItem + { + Name = AppResources.LogOut, + ExecuteAsync = () => LogOutAsync() + }); if (_showChangeMasterPassword) { accountItems.Insert(0, new SettingsPageListItem @@ -791,5 +807,13 @@ namespace Bit.App.Pages await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok); } } + + private async Task ToggleWatchConnectionAsync() + { + _shouldConnectToWatch = !_shouldConnectToWatch; + + await _watchDeviceService.SetShouldConnectToWatchAsync(_shouldConnectToWatch); + BuildList(); + } } } diff --git a/src/App/Pages/Vault/CipherAddEditPageViewModel.cs b/src/App/Pages/Vault/CipherAddEditPageViewModel.cs index 4f8ea42a1..67ae693e0 100644 --- a/src/App/Pages/Vault/CipherAddEditPageViewModel.cs +++ b/src/App/Pages/Vault/CipherAddEditPageViewModel.cs @@ -29,6 +29,7 @@ namespace Bit.App.Pages private readonly ICustomFieldItemFactory _customFieldItemFactory; private readonly IClipboardService _clipboardService; private readonly IAutofillHandler _autofillHandler; + private readonly IWatchDeviceService _watchDeviceService; private bool _showNotesSeparator; private bool _showPassword; @@ -80,6 +81,7 @@ namespace Bit.App.Pages _customFieldItemFactory = ServiceContainer.Resolve("customFieldItemFactory"); _clipboardService = ServiceContainer.Resolve("clipboardService"); _autofillHandler = ServiceContainer.Resolve(); + _watchDeviceService = ServiceContainer.Resolve(); GeneratePasswordCommand = new Command(GeneratePassword); TogglePasswordCommand = new Command(TogglePassword); @@ -507,6 +509,8 @@ namespace Bit.App.Pages EditMode && !CloneMode ? AppResources.ItemUpdated : AppResources.NewItemCreated); _messagingService.Send(EditMode && !CloneMode ? "editedCipher" : "addedCipher", Cipher.Id); + _watchDeviceService.SyncDataToWatchAsync().FireAndForget(); + if (Page is CipherAddEditPage page && page.FromAutofillFramework) { // Close and go back to app diff --git a/src/App/Pages/Vault/CipherDetailsPageViewModel.cs b/src/App/Pages/Vault/CipherDetailsPageViewModel.cs index dcd994335..f0005ecf2 100644 --- a/src/App/Pages/Vault/CipherDetailsPageViewModel.cs +++ b/src/App/Pages/Vault/CipherDetailsPageViewModel.cs @@ -31,6 +31,7 @@ namespace Bit.App.Pages private readonly ILocalizeService _localizeService; private readonly ICustomFieldItemFactory _customFieldItemFactory; private readonly IClipboardService _clipboardService; + private readonly IWatchDeviceService _watchDeviceService; private List _fields; private bool _canAccessPremium; @@ -62,6 +63,7 @@ namespace Bit.App.Pages _localizeService = ServiceContainer.Resolve("localizeService"); _customFieldItemFactory = ServiceContainer.Resolve("customFieldItemFactory"); _clipboardService = ServiceContainer.Resolve("clipboardService"); + _watchDeviceService = ServiceContainer.Resolve(); CopyCommand = new AsyncCommand((id) => CopyAsync(id, null), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false); CopyUriCommand = new AsyncCommand(uriView => CopyAsync("LoginUri", uriView.Uri), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false); @@ -371,6 +373,9 @@ namespace Bit.App.Pages await _cipherService.SoftDeleteWithServerAsync(Cipher.Id); } await _deviceActionService.HideLoadingAsync(); + + _watchDeviceService.SyncDataToWatchAsync().FireAndForget(); + _platformUtilsService.ShowToast("success", null, Cipher.IsDeleted ? AppResources.ItemDeleted : AppResources.ItemSoftDeleted); _messagingService.Send(Cipher.IsDeleted ? "deletedCipher" : "softDeletedCipher", Cipher); diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index ca0e23b08..7285cbeaa 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -1498,6 +1498,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Connect to Watch. + /// + public static string ConnectToWatch { + get { + return ResourceManager.GetString("ConnectToWatch", resourceCulture); + } + } + /// /// Looks up a localized string similar to Continue. /// diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 82ed75f6b..ee49befd5 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -2453,6 +2453,9 @@ select Add TOTP to store the key safely Random + + Connect to Watch + Accessibility Service Disclosure diff --git a/src/App/Services/BaseWatchDeviceService.cs b/src/App/Services/BaseWatchDeviceService.cs new file mode 100644 index 000000000..f2ffc0090 --- /dev/null +++ b/src/App/Services/BaseWatchDeviceService.cs @@ -0,0 +1,128 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models; +using Bit.Core.Models.View; +using Xamarin.Forms; + +namespace Bit.App.Services +{ + public abstract class BaseWatchDeviceService : IWatchDeviceService + { + private readonly ICipherService _cipherService; + private readonly IEnvironmentService _environmentService; + private readonly IStateService _stateService; + private readonly IVaultTimeoutService _vaultTimeoutService; + + protected BaseWatchDeviceService(ICipherService cipherService, + IEnvironmentService environmentService, + IStateService stateService, + IVaultTimeoutService vaultTimeoutService) + { + _cipherService = cipherService; + _environmentService = environmentService; + _stateService = stateService; + _vaultTimeoutService = vaultTimeoutService; + } + + public abstract bool IsConnected { get; } + + protected abstract bool CanSendData { get; } + protected abstract bool IsSupported { get; } + + public async Task SyncDataToWatchAsync() + { + if (!IsSupported) + { + return; + } + + var shouldConnect = await _stateService.GetShouldConnectToWatchAsync(); + if (shouldConnect && !IsConnected) + { + ConnectToWatch(); + } + + if (!CanSendData) + { + return; + } + + var userData = await _stateService.GetActiveUserCustomDataAsync(a => a?.Profile is null ? null : new WatchDTO.UserDataDto + { + Id = a.Profile.UserId, + Name = a.Profile.Name, + Email = a.Profile.Email + }); + var state = await GetStateAsync(userData?.Id, shouldConnect); + if (state != WatchState.Valid) + { + await SendDataToWatchAsync(new WatchDTO(state)); + return; + } + + var ciphersWithTotp = await _cipherService.GetAllDecryptedAsync(c => c.DeletedDate == null && c.Login?.Totp != null); + + if (!ciphersWithTotp.Any()) + { + await SendDataToWatchAsync(new WatchDTO(WatchState.Need2FAItem)); + return; + } + + var watchDto = new WatchDTO(state) + { + Ciphers = ciphersWithTotp.Select(c => new SimpleCipherView(c)).ToList(), + UserData = userData, + EnvironmentData = new WatchDTO.EnvironmentUrlDataDto + { + Base = _environmentService.BaseUrl, + Icons = _environmentService.IconsUrl + } + //SettingsData = new WatchDTO.SettingsDataDto + //{ + // VaultTimeoutInMinutes = await _vaultTimeoutService.GetVaultTimeout(userData?.Id), + // VaultTimeoutAction = await _stateService.GetVaultTimeoutActionAsync(userData?.Id) ?? VaultTimeoutAction.Lock + //} + }; + await SendDataToWatchAsync(watchDto); + } + + private async Task GetStateAsync(string userId, bool shouldConnectToWatch) + { + if (!shouldConnectToWatch) + { + return WatchState.NeedSetup; + } + + if (!await _stateService.IsAuthenticatedAsync() || userId is null) + { + return WatchState.NeedLogin; + } + + //if (await _vaultTimeoutService.IsLockedAsync() || + // await _vaultTimeoutService.ShouldLockAsync()) + //{ + // return WatchState.NeedUnlock; + //} + + if (!await _stateService.CanAccessPremiumAsync(userId)) + { + return WatchState.NeedPremium; + } + + return WatchState.Valid; + } + + public async Task SetShouldConnectToWatchAsync(bool shouldConnectToWatch) + { + await _stateService.SetShouldConnectToWatchAsync(shouldConnectToWatch); + await SyncDataToWatchAsync(); + } + + protected abstract Task SendDataToWatchAsync(WatchDTO watchDto); + + protected abstract void ConnectToWatch(); + } +} diff --git a/src/App/Utilities/AccountManagement/AccountsManager.cs b/src/App/Utilities/AccountManagement/AccountsManager.cs index b804823f2..c87671073 100644 --- a/src/App/Utilities/AccountManagement/AccountsManager.cs +++ b/src/App/Utilities/AccountManagement/AccountsManager.cs @@ -22,6 +22,8 @@ namespace Bit.App.Utilities.AccountManagement private readonly IAuthService _authService; private readonly ILogger _logger; private readonly IMessagingService _messagingService; + private readonly IWatchDeviceService _watchDeviceService; + Func _getOptionsFunc; private IAccountsManagerHost _accountsManagerHost; @@ -32,7 +34,8 @@ namespace Bit.App.Utilities.AccountManagement IPlatformUtilsService platformUtilsService, IAuthService authService, ILogger logger, - IMessagingService messagingService) + IMessagingService messagingService, + IWatchDeviceService watchDeviceService) { _broadcasterService = broadcasterService; _vaultTimeoutService = vaultTimeoutService; @@ -42,6 +45,7 @@ namespace Bit.App.Utilities.AccountManagement _authService = authService; _logger = logger; _messagingService = messagingService; + _watchDeviceService = watchDeviceService; } private AppOptions Options => _getOptionsFunc?.Invoke() ?? new AppOptions { IosExtension = true }; @@ -145,6 +149,7 @@ namespace Bit.App.Utilities.AccountManagement break; case AccountsManagerMessageCommands.SWITCHED_ACCOUNT: await SwitchedAccountAsync(); + break; } } @@ -217,6 +222,7 @@ namespace Bit.App.Utilities.AccountManagement } await Task.Delay(50); await _accountsManagerHost.UpdateThemeAsync(); + _watchDeviceService.SyncDataToWatchAsync().FireAndForget(); _messagingService.Send(AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED); }); } diff --git a/src/Core/Abstractions/ICipherService.cs b/src/Core/Abstractions/ICipherService.cs index 60e4b722c..e34aaa068 100644 --- a/src/Core/Abstractions/ICipherService.cs +++ b/src/Core/Abstractions/ICipherService.cs @@ -19,7 +19,7 @@ namespace Bit.Core.Abstractions Task DeleteWithServerAsync(string id); Task EncryptAsync(CipherView model, SymmetricCryptoKey key = null, Cipher originalCipher = null); Task> GetAllAsync(); - Task> GetAllDecryptedAsync(); + Task> GetAllDecryptedAsync(Func filter = null); Task, List, List>> GetAllDecryptedByUrlAsync(string url, List includeOtherTypes = null); Task> GetAllDecryptedForGroupingAsync(string groupingId, bool folder = true); diff --git a/src/Core/Abstractions/IStateService.cs b/src/Core/Abstractions/IStateService.cs index 63a020403..9525ed481 100644 --- a/src/Core/Abstractions/IStateService.cs +++ b/src/Core/Abstractions/IStateService.cs @@ -14,6 +14,7 @@ namespace Bit.Core.Abstractions List AccountViews { get; } Task GetActiveUserIdAsync(); Task GetActiveUserEmailAsync(); + Task GetActiveUserCustomDataAsync(Func dataMapper); Task IsActiveAccountAsync(string userId = null); Task SetActiveUserAsync(string userId); Task CheckExtensionActiveUserAndSwitchIfNeededAsync(); @@ -159,5 +160,7 @@ namespace Bit.Core.Abstractions 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); } } diff --git a/src/Core/Abstractions/IWatchDeviceService.cs b/src/Core/Abstractions/IWatchDeviceService.cs new file mode 100644 index 000000000..0edfb2afe --- /dev/null +++ b/src/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/Core/Constants.cs b/src/Core/Constants.cs index c31b0d175..06d3a19a7 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -96,5 +96,6 @@ public static string BiometricUnlockKey(string userId) => $"biometricUnlock_{userId}"; public static string ApprovePasswordlessLoginsKey(string userId) => $"approvePasswordlessLogins_{userId}"; public static string UsernameGenOptionsKey(string userId) => $"usernameGenerationOptions_{userId}"; + public static string ShouldConnectToWatchKey(string userId) => $"shouldConnectToWatch_{userId}"; } } diff --git a/src/Core/Enums/WatchState.cs b/src/Core/Enums/WatchState.cs new file mode 100644 index 000000000..8452e8d68 --- /dev/null +++ b/src/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/Core/Models/View/SimpleCipherView.cs b/src/Core/Models/View/SimpleCipherView.cs new file mode 100644 index 000000000..df5262710 --- /dev/null +++ b/src/Core/Models/View/SimpleCipherView.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; +using Bit.Core.Enums; + +namespace Bit.Core.Models.View +{ + public class 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() + }; + } + } + + public string Id { get; set; } + public string Name { get; set; } + public CipherType Type { get; set; } + public SimpleLoginView Login { get; set; } + } + + public class SimpleLoginView + { + public string Username { get; set; } + public string Totp { get; set; } + public List Uris { get; set; } + } + + public class SimpleLoginUriView + { + public SimpleLoginUriView(string uri) + { + Uri = uri; + } + + public string Uri { get; set; } + } +} + diff --git a/src/Core/Models/View/WatchDTO.cs b/src/Core/Models/View/WatchDTO.cs new file mode 100644 index 000000000..33bcae973 --- /dev/null +++ b/src/Core/Models/View/WatchDTO.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Bit.Core.Enums; +using Bit.Core.Models.View; + +namespace Bit.Core.Models +{ + public class WatchDTO + { + public WatchDTO(WatchState state) + { + State = state; + } + + public WatchState State { get; private set; } + + public List Ciphers { get; set; } + + public UserDataDto UserData { get; set; } + + public EnvironmentUrlDataDto EnvironmentData { get; set; } + + //public SettingsDataDto SettingsData { get; set; } + + public class UserDataDto + { + public string Id { get; set; } + public string Email { get; set; } + public string Name { get; set; } + } + + public class EnvironmentUrlDataDto + { + public string Base { get; set; } + public string Icons { get; set; } + } + + //public class SettingsDataDto + //{ + // public int? VaultTimeoutInMinutes { get; set; } + + // public VaultTimeoutAction VaultTimeoutAction { get; set; } + //} + } +} diff --git a/src/Core/Services/AuthService.cs b/src/Core/Services/AuthService.cs index 4e95cf847..91f633b37 100644 --- a/src/Core/Services/AuthService.cs +++ b/src/Core/Services/AuthService.cs @@ -26,6 +26,8 @@ namespace Bit.Core.Services private readonly IKeyConnectorService _keyConnectorService; private readonly IPasswordGenerationService _passwordGenerationService; private readonly bool _setCryptoKeys; + + private readonly LazyResolve _watchDeviceService = new LazyResolve(); private SymmetricCryptoKey _key; public AuthService( @@ -187,6 +189,7 @@ namespace Bit.Core.Services { callback.Invoke(); _messagingService.Send(AccountsManagerMessageCommands.LOGGED_OUT); + _watchDeviceService.Value.SyncDataToWatchAsync().FireAndForget(); } public List GetSupportedTwoFactorProviders() diff --git a/src/Core/Services/CipherService.cs b/src/Core/Services/CipherService.cs index bf48e80d8..e289a1ae6 100644 --- a/src/Core/Services/CipherService.cs +++ b/src/Core/Services/CipherService.cs @@ -226,7 +226,7 @@ namespace Bit.Core.Services return response?.ToList() ?? new List(); } - public async Task> GetAllDecryptedAsync() + public async Task> GetAllDecryptedAsync(Func filter = null) { if (_clearCipherCacheKey != null) { @@ -237,7 +237,7 @@ namespace Bit.Core.Services await _storageService.RemoveAsync(_clearCipherCacheKey); } } - if (DecryptedCipherCache != null) + if (DecryptedCipherCache != null && filter is null) { return DecryptedCipherCache; } @@ -261,13 +261,24 @@ namespace Bit.Core.Services decCiphers.Add(c); } var tasks = new List(); - var ciphers = await GetAllAsync(); + 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; } diff --git a/src/Core/Services/StateService.cs b/src/Core/Services/StateService.cs index e13efadd6..72da24ff0 100644 --- a/src/Core/Services/StateService.cs +++ b/src/Core/Services/StateService.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Bit.Core.Abstractions; @@ -52,6 +51,15 @@ namespace Bit.Core.Services 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) @@ -1685,5 +1693,21 @@ namespace Bit.Core.Services } 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()); + var key = Constants.ShouldConnectToWatchKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions) ?? false; + } + + public async Task SetShouldConnectToWatchAsync(bool shouldConnect, string userId = null) + { + var reconciledOptions = + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()); + var key = Constants.ShouldConnectToWatchKey(reconciledOptions.UserId); + await SetValueAsync(key, shouldConnect, reconciledOptions); + } } } diff --git a/src/Core/Services/SyncService.cs b/src/Core/Services/SyncService.cs index cf3db3a7e..7eeaeda31 100644 --- a/src/Core/Services/SyncService.cs +++ b/src/Core/Services/SyncService.cs @@ -27,6 +27,8 @@ namespace Bit.Core.Services private readonly ILogger _logger; private readonly Func, Task> _logoutCallbackAsync; + private readonly LazyResolve _watchDeviceService = new LazyResolve(); + public SyncService( IStateService stateService, IApiService apiService, @@ -112,6 +114,8 @@ namespace Bit.Core.Services await SyncPoliciesAsync(response.Policies); await SyncSendsAsync(userId, response.Sends); await SetLastSyncAsync(now); + _watchDeviceService.Value.SyncDataToWatchAsync().FireAndForget(); + return SyncCompleted(true); } catch diff --git a/src/iOS.Core/Services/WatchDeviceService.cs b/src/iOS.Core/Services/WatchDeviceService.cs new file mode 100644 index 000000000..89fda8da9 --- /dev/null +++ b/src/iOS.Core/Services/WatchDeviceService.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.App.Services; +using Bit.Core.Abstractions; +using Bit.Core.Models; +using Newtonsoft.Json; +using WatchConnectivity; + +namespace Bit.iOS.Core.Services +{ + public class WatchDeviceService : BaseWatchDeviceService + { + public WatchDeviceService(ICipherService cipherService, + IEnvironmentService environmentService, + IStateService stateService, + IVaultTimeoutService vaultTimeoutService) + : base(cipherService, environmentService, stateService, vaultTimeoutService) + { + } + + public override bool IsConnected => WCSessionManager.SharedManager.IsSessionActivated; + + protected override bool CanSendData => WCSessionManager.SharedManager.IsValidSession; + + protected override bool IsSupported => WCSession.IsSupported; + + protected override Task SendDataToWatchAsync(WatchDTO watchDto) + { + var serializedData = JsonConvert.SerializeObject(watchDto); + + // Add time to the key to make it change on every message sent so it's delivered faster. + // If we use the same key then the OS may defer the delivery of the message because of + // resources, reachability and other stuff + WCSessionManager.SharedManager.SendBackgroundHighPriorityMessage(new Dictionary + { + [$"watchDto-{DateTime.UtcNow.ToLongTimeString()}"] = serializedData + }); + + return Task.CompletedTask; + } + + protected override void ConnectToWatch() + { + WCSessionManager.SharedManager.StartSession(); + } + } +} diff --git a/src/iOS.Core/Utilities/DictionaryExtensions.cs b/src/iOS.Core/Utilities/DictionaryExtensions.cs new file mode 100644 index 000000000..38379209a --- /dev/null +++ b/src/iOS.Core/Utilities/DictionaryExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Foundation; +using Newtonsoft.Json; + +namespace Bit.iOS.Core.Utilities +{ + public static class DictionaryExtensions + { + public static NSDictionary ToNSDictionary(this Dictionary dict) + { + return dict.ToNSDictionary(k => new NSString(k), v => (NSObject)new NSString(JsonConvert.SerializeObject(v))); + } + + public static NSDictionary ToNSDictionary(this Dictionary dict, Func keyConverter, Func valueConverter) + where KTo : NSObject + where VTo : NSObject + { + var NSValues = dict.Values.Select(x => valueConverter(x)).ToArray(); + var NSKeys = dict.Keys.Select(x => keyConverter(x)).ToArray(); + return NSDictionary.FromObjectsAndKeys(NSValues, NSKeys, NSKeys.Count()); + } + } +} + diff --git a/src/iOS.Core/Utilities/WCSessionManager.cs b/src/iOS.Core/Utilities/WCSessionManager.cs new file mode 100644 index 000000000..34cb8b254 --- /dev/null +++ b/src/iOS.Core/Utilities/WCSessionManager.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.iOS.Core.Utilities; +using Foundation; +using Newtonsoft.Json; + +namespace WatchConnectivity +{ + public sealed class WCSessionManager : WCSessionDelegate + { + // Setup is converted from https://www.natashatherobot.com/watchconnectivity-say-hello-to-wcsession/ + // with some extra bits + private static readonly WCSessionManager sharedManager = new WCSessionManager(); + private static WCSession session = WCSession.IsSupported ? WCSession.DefaultSession : null; + + public static string Device = "Phone"; + + public event WCSessionReceiveDataHandler ApplicationContextUpdated; + public event WCSessionReceiveDataHandler MessagedReceived; + public delegate void WCSessionReceiveDataHandler(WCSession session, Dictionary applicationContext); + + + private WCSession validSession + { + get + { + Console.WriteLine($"Paired status:{(session.Paired ? '✓' : '✗')}\n"); + Console.WriteLine($"Watch App Installed status:{(session.WatchAppInstalled ? '✓' : '✗')}\n"); + return (session.Paired && session.WatchAppInstalled) ? session : null; + } + } + + private WCSession validReachableSession + { + get + { + return session.Reachable ? validSession : null; + } + } + + public bool IsValidSession => validSession != null; + + public bool IsSessionReachable => session.Reachable; + + public bool IsSessionActivated => validSession?.ActivationState == WCSessionActivationState.Activated; + + private WCSessionManager() : base() { } + + public static WCSessionManager SharedManager + { + get + { + return sharedManager; + } + } + + public void StartSession() + { + if (session != null) + { + session.Delegate = this; + session.ActivateSession(); + Console.WriteLine($"Started Watch Connectivity Session on {Device}"); + } + } + + public override void SessionReachabilityDidChange(WCSession session) + { + Console.WriteLine($"Watch connectivity Reachable:{(session.Reachable ? '✓' : '✗')} from {Device}"); + // handle session reachability change + if (session.Reachable) + { + // great! continue on with Interactive Messaging + } + else + { + // 😥 prompt the user to unlock their iOS device + } + } + + #region Application Context Methods + + public void SendBackgroundHighPriorityMessage(Dictionary applicationContext) + { + // Application context doesnt need the watch to be reachable, it will be received when opened + if (validSession is null || validSession.ActivationState != WCSessionActivationState.Activated) + { + return; + } + + Xamarin.Forms.Device.BeginInvokeOnMainThread(() => + { + try + { + var sendSuccessfully = validSession.UpdateApplicationContext(applicationContext.ToNSDictionary(), out var error); + if (sendSuccessfully) + { + Console.WriteLine($"Sent App Context from {Device} \nPayLoad: {applicationContext.ToNSDictionary().ToString()} \n"); + } + else + { + Console.WriteLine($"Error Updating Application Context: {error.LocalizedDescription}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Exception Updating Application Context: {ex.Message}"); + } + }); + } + WCSessionUserInfoTransfer _transf; + public void SendBackgroundFifoHighPriorityMessage(Dictionary message) + { + if(validSession is null || validSession.ActivationState != WCSessionActivationState.Activated) + { + return; + } + + _transf?.Cancel(); + + Console.WriteLine("Started transferring user info"); + + _transf = session.TransferUserInfo(message.ToNSDictionary()); + + + Task.Run(async () => + { + try + { + while (_transf.Transferring) + { + await Task.Delay(1000); + } + Console.WriteLine("Finished transferring user info"); + } + catch (Exception ex) + { + Console.WriteLine("Error transferring user info " + ex); + } + }); + + //session.SendMessage(dic, + // (dd) => + // { + // Console.WriteLine(dd?.ToString()); + // }, + // error => + // { + // Console.WriteLine(error?.ToString()); + // } + //); + } + + public override void DidReceiveApplicationContext(WCSession session, NSDictionary applicationContext) + { + Console.WriteLine($"Receiving Message on {Device}"); + if (ApplicationContextUpdated != null) + { + var keys = applicationContext.Keys.Select(k => k.ToString()).ToArray(); + var values = applicationContext.Values.Select(v => JsonConvert.DeserializeObject(v.ToString())).ToArray(); + var dictionary = keys.Zip(values, (k, v) => new { Key = k, Value = v }) + .ToDictionary(x => x.Key, x => x.Value); + + ApplicationContextUpdated(session, dictionary); + } + } + + + public override void DidReceiveMessage(WCSession session, NSDictionary message) + { + Console.WriteLine($"Receiving Message on {Device}"); + + var keys = message.Keys.Select(k => k.ToString()).ToArray(); + var values = message.Values.Select(v => v?.ToString() as object).ToArray(); + var dictionary = keys.Zip(values, (k, v) => new { Key = k, Value = v }) + .ToDictionary(x => x.Key, x => x.Value); + + MessagedReceived?.Invoke(session, dictionary); + } + + #endregion + } +} diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs index 4d6a13c6c..41afc0bfc 100644 --- a/src/iOS.Core/Utilities/iOSCoreHelpers.cs +++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs @@ -48,6 +48,12 @@ namespace Bit.iOS.Core.Utilities clearCipherCacheKey, Bit.Core.Constants.iOSAllClearCipherCacheKeys); InitLogger(); + + ServiceContainer.Register(new WatchDeviceService(ServiceContainer.Resolve(), + ServiceContainer.Resolve(), + ServiceContainer.Resolve(), + ServiceContainer.Resolve())); + Bootstrap(); var appOptions = new AppOptions { IosExtension = true }; @@ -226,7 +232,8 @@ namespace Bit.iOS.Core.Utilities ServiceContainer.Resolve("platformUtilsService"), ServiceContainer.Resolve("authService"), ServiceContainer.Resolve("logger"), - ServiceContainer.Resolve("messagingService")); + ServiceContainer.Resolve("messagingService"), + ServiceContainer.Resolve()); ServiceContainer.Register("accountsManager", accountsManager); if (postBootstrapFunc != null) diff --git a/src/iOS.Core/iOS.Core.csproj b/src/iOS.Core/iOS.Core.csproj index a4515c9b8..f52468e43 100644 --- a/src/iOS.Core/iOS.Core.csproj +++ b/src/iOS.Core/iOS.Core.csproj @@ -204,9 +204,12 @@ + + + diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index 25b9049e2..ef1544ea8 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -11,12 +11,13 @@ using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Utilities; +using Bit.iOS.Core.Services; using Bit.iOS.Core.Utilities; using Bit.iOS.Services; using CoreNFC; using Foundation; using UIKit; -using UserNotifications; +using WatchConnectivity; using Xamarin.Forms; using Xamarin.Forms.Platform.iOS; @@ -57,6 +58,9 @@ namespace Bit.iOS LoadApplication(new App.App(null)); iOSCoreHelpers.AppearanceAdjustments(); ZXing.Net.Mobile.Forms.iOS.Platform.Init(); + + ConnectToWatchIfNeededAsync().FireAndForget(); + _broadcasterService.Subscribe(nameof(AppDelegate), async (message) => { try @@ -302,6 +306,12 @@ namespace Bit.iOS ServiceContainer.Init(deviceActionService.DeviceUserAgent, Constants.ClearCiphersCacheKey, Constants.iOSAllClearCipherCacheKeys); iOSCoreHelpers.InitLogger(); + + ServiceContainer.Register(new WatchDeviceService(ServiceContainer.Resolve(), + ServiceContainer.Resolve(), + ServiceContainer.Resolve(), + ServiceContainer.Resolve())); + _pushHandler = new iOSPushNotificationHandler( ServiceContainer.Resolve("pushNotificationListenerService")); _nfcDelegate = new Core.NFCReaderDelegate((success, message) => @@ -393,5 +403,13 @@ namespace Bit.iOS await AppHelpers.SetPreconfiguredSettingsAsync(dict); } } + + private async Task ConnectToWatchIfNeededAsync() + { + if (_stateService != null && await _stateService.GetShouldConnectToWatchAsync()) + { + WCSessionManager.SharedManager.StartSession(); + } + } } } diff --git a/src/iOS/iOS.csproj b/src/iOS/iOS.csproj index 0e85b0d9e..600697876 100644 --- a/src/iOS/iOS.csproj +++ b/src/iOS/iOS.csproj @@ -134,6 +134,15 @@ + + $(Home)/Library/Developer/Xcode/DerivedData/bitwarden-cbtqsueryycvflfzbsoteofskiyr/Build/Products + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..'))/watchOS/bitwarden.xcarchive/Products/Applications/bitwarden.app/Watch + Bitwarden.app + watchsimulator + watchos + $(WatchAppBuildPath)/$(Configuration)-$(WatchAppConfiguration)/$(WatchAppBundle) + $(WatchAppBuildPath)/$(WatchAppBundle) + @@ -406,4 +415,17 @@ + + <_ResolvedWatchAppReferences Include="$(WatchAppBundleFullPath)" /> + + + <_ResolvedWatchAppReferences Include="$(WatchAppBundleFullPath)" /> + + + --deep + + + + + \ No newline at end of file diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AccentColor.colorset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..14b11821c --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.349", + "green" : "0.664", + "red" : "0.279" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/100.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 0000000000000000000000000000000000000000..eb753fbceb40ae736302c87d4f7c6dbbb18cd981 GIT binary patch literal 3995 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 BWUK%H literal 0 HcmV?d00001 diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/102.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/102.png new file mode 100644 index 0000000000000000000000000000000000000000..7050b62688f7fd5a1a71f5776dfa6be20be37dfd GIT binary patch literal 3451 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 ARsaA1 literal 0 HcmV?d00001 diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/234.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/234.png new file mode 100644 index 0000000000000000000000000000000000000000..7251b0ea618c6ac5452e5d21ba3465883ff0e656 GIT binary patch literal 6753 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 JPDHLkV1g`i!jb?0 literal 0 HcmV?d00001 diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/66.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/66.png new file mode 100644 index 0000000000000000000000000000000000000000..66f36732d5a133cfd9f375a04d455376278831e8 GIT binary patch literal 2393 zcmY*bc|6qH8~)NXb~VN%R5UIXGt43h5oRo*>|17J8KrC^)JUm_u~*iigj-OvEOljf8Eddob!Ipd!Fa>zVGw=b7Cwn8jA_b2m=5hW@>`7=0$_O zEePhFQ?>w`7eH6_E%X7PJV|8BZ9nfW?_pwX0RW*&0B}DN05*83`#fAQ3IP7T0svYW z03a3ctlmn8_W*V`GsXeCdn@NHJsAK5159!Hwzo%rWKt|89gcNOyl8NOo`G4oyPT21 zC)BF45_0mWpe2?5)_pnv45Sa@D{WQrvaqW#RUR21_0c$-T4eNELgh}Y2o$CUVxUE_ zEdaI!z3(RZ?%LbA`pbp1Cu=zZrk^ZVPxUSC{9ap|kMO&X&>rQNL&}>aX|xz)JD)08 zEcb&dxMfh?XYz64XD1_K9RvFKNpvY%FRhF<8y<#OH)y(e#Wsea7$81}SxSWamE=Y2JD(ece!{D`8yZ zb4;+uQ6~r5h~U`yl(RH3?zbB!>Nh)#Cp0O%qzOA4OvPkHU7b@8z5{V3__N@iUJ5rc z$b*W1v>4!ncCoA@FtoN|m?~I^i84%anxB2~h+0~r4SA}nD-}^TiKEqx#Sb{7!rW?V z`Zp7gMpyW1BAD7&efwz61pL;wX#&Yh0efr6Fy&iSs4^qBfcn3BdxW05{{#K4BMUi;mB_G;OC}yHUkEtYn>No7 zeE$n!ppgX1A3UGpt$ibaQ$Zj@%np27@u8{4pOvom!*6Y88fqn(k9j{lY4g;A%MgL5 z9Vwa`!4UGT4L;rop!Yl%)OJ-(HqQUZpJ#p%!-{0Ul?$nD0yii%=2A+2pZ%b)$&Htr zq{QcH=}ZNiYkXEpKXTxzBa%|(>|&X0+~OWIvDS0zC3etb$K&yNHx-t z>m|0=W5$!~K`L?qAwICsiC9m+@|*lP6SJghUF9*y^nem)BQK0{N6aJh`)C$KBGN!Q&RN9Nw@~ zrygj(9*=wDKGjX)P{YF%aq9#Z#huyxkkW55?oIg(~jJ)oNj~jBxi{hzQ30?Cf+z;-rrix6yZP ziLDFcq4Cs1OU=0~{+jQPFS9m09zfG&P;evFq#pfGOlg?#!AeF<(58RrO>xwNX@NO@v65fLi&jxZkyvYLzRD|+FB(HOyeO6IE zwo}O>UTydu^G|MUqm^T#N+0wt8oodf@MV=DY$cx6LCG?CTznx}y}_Og!G_GEJR@!& z0BfIoz;rHnO$bN*TpZEjn$A8sF54~NmD7pw+3dTYW>3O{=-0f-JkJ}7?lnkg7>`}z zjI#BhdT7oJhT@4r@~|ebS<>HE)N4;F!>`f75qY+c=UK)>yWFhM^u|%}hDJjv|2~Bo zj-ZI-qnN|egeWsg*S2{cV=#W6DMNIr>M;9mCusV~z$4%-q69XI*81J7pG->-jYm8Z z-5yGXPhl1G_NPifGHfqKLl@2u$sVzb)eFD8q_NZ_1O*o*XT|6fon85*!9^;~>duW- z)-=5XeO3@e(oF2aZdq<&=`_Ut*?(Edd;xk+50Jh-S&IVzpFWHd@)NBiVlv> z6nFfxcrUym|L`QuIZ0r0?dtX0wTV5r;i!r-11PIIptdKMN<9@r()nq%xv*vgIX8O! z3nFCqdt_L`zGUcV?+S=Qwt#3c{E2D?gAJjz^nxNm4M6sn)uy{dpz!#_%i=`))<#$6 z(08`Z%L!S%ygz@@`8ww3hiP-d&kKc;YhJStF0S?8T1^t{K1pAaTR?9ksPGC_gjirJ5fn zrytqvXCr#wIo~fYZ_^APhi`}FzJj-Rr1njU^hI-yycfP;KZ!W^(E^#>qB^b76n;a< z)ip-iPA0_25;7@G`o*be8d-448y|^U>!qu|G%^gP&9>q}-uKj4eBQ&N#Z;GX!BeAQu^lQ#&L60peW)njXL@0;^usu(UuD<^$ zVV-vANVmqf8L|C3-B|=CwtKNTtBh}WuCL1a*>1C_vCSr3py85BXLa9ZRBX1>Ztt?v z^!%$^{V<(suIfffm*F5;?5=BB-CY?{dh^j#G4zsm|I(_?UltCl3h#)40tEb}uH?AI zCjafbFSiQ$*9Jp&iqr0z^$ZO^*(uD{Eu8%s92(-L2A;3WSgeYp1or#>$`MU$Oj=z^ z8K`{u?5hB0eHVMo_yk9Cd}S)HgE4m7y%{OKxj_y&e(q3(gl=akyMI|9uY=Z;(hbzV zCryUx`aRr&v%q0rn!*9e$vkqrN=qZ}H=-DXcMS4$3nFQG1d@0Gz@RV~BPx@6G=otRCr$Poe6MMMHGltCe2B9>WW;5v#p`J;)achI;{f z5HG-ntt|H{*n^1K&{xX5fISI#h!?O2@d9k{ic+tFJ&2eMTJ8nxLB3!Emz8)G>_NgT zSpYBtK(#}EYUnrmbtfDS!0x#IZXya~$uAenzMd{5ur@nTQtp65_MIA6tuO`z`7>xW zPS7H{VFPmH1Ezvh+8prrWk~GcgBXK`mqw&Ow-YtjhXc4!Xvg8>R+L*E@YOOQ;jpBo z7J0g3;14DRFHa3X`W_?;D@L5$m=oP#AUG0UnO0$fz=Kw3{fOpf>GUPV8<6Apq& zs@?Nd1-5-rj<8?>zzK^@LV%y?;fHDc19%PJNr35Hb{;Foc2U6bf`C(11}wL!x@%b) zXI!pvNMd8#LFm1vvA1y8jLWoGa+TeO@RMQZNCYJ|NJe9xMT-tY!F06ACR3 z_-MremsU8ibA}EbTlv6l2gokykN|K0q6}Me%U}=*a=irsk5Lh@xzvGi>&-A0D+9Q! z!h!XZLeT#1x&aGi5xo}`3MqI@uK?8)On}qYns|Dsgn|k1hABEcuaIb9n zfD4Nq7~3lVGm_P@2m+kCMq$8Hb$Hkf;BC33*mAfGh7f@~xaYzG*$U=>dk0` zOn{$x&8Vn?H>T_B1ibn4QZc|i1Obmz8Sv8z1Kv~z;H|l(5C;5Y)-@pokLw+XSp(Hm z@KdiADXQSj1o$4`TC<3)M@q0sRKZUyxGEp8#ewmC32?A#7BPBtp(2236p+%i&C^2B z!42RohY4_rm_>9~0Pt)T0gqA`@RnNu-Y5#V+x&bX1yAT3h}nZeR8#QCRRxMFc5>)Wxnc{$-AFLkW5i6Aiylr|NfIlzB22sF|&ATF`;EDZ$pdMg5QeIIG zFm*JxPB)-qtJ;IyCIWoW0z-&K{($?Vf`BKf2-sArEMN+9+iwB*O<}-uFAFHxj*0z( zF=vQQ6%XcsS6oxfgG(wL*fE0?>{n}=w&{=s!hpMG2m+1|Muu8|OPqiUis@^w*k~H1 zX$1JKneG8^JY>Ne2P|kJVi8^DToO|7r1%hI3{^?NC3Xy7aTNs?`6w{k;p?M}2k)BQ)GgpQ4-jCJm_>9H z08C1nlAtc&!3qQ3T?gPdlmOge)*rGJECJ3O9;%u}40`#Bq6*&qd{cC4?O$t}_QnAd z)_p3d;D=`l0#4LnzKVbc<|qqz&m8xF*MFt};D=`92`P9g0Zs{1O~J`aFQdS0gRhU+ ztVbyi-a97(o$jq&!322iCq^+3ZZ811kUTh1hXpDE9-tuL(sBp(W;g-6PW_VpP62M+ z$3_@*0%H;Fr=J&7aDTnJfR|iSPQm34yfZh_J>XA`So^UNO>|n=Xm+EFgSC533~)LD z9-*E^BrR4L@VrQLcBkM^jaai^5b%T3&dI78t%jk%Vnce89$6y{s(En#mo6%*;CC~l z+yY+r2>~t=Q}6>KfXz0f571+wihvUp2E6YUfM45Rgm9e}R=F1Or}VS(6>PR)YLXs{ zMm14I!Nw9h5?{QasDj^{PYSL%c~J^-1b6Mnf(pKW>KWO93oSOJ_10s>*l^VVC%o{7 zB7hlVd;!n?>}WjDqV};1xwAulEpa!`)G2COT7rVI3uAH$sAN&7dL72#RaGNQoWhq#iS8!i*KO9=z z+@s3}Tmi=~$`b~xWq`TNfydhh;Jx|LHQ-A6ASgp}F5txJa=vCxrUirDH(4HFQUN`b zW;q^N8jH9HACIZn6>z_7alk%WhN~tUa#F)FIWZJg8_m{qHb3cESBh-tw~PVW?2lZQU|G3WK_acxdo!@GrGLwZz=5h*3D*cEV}?DMjN9NmY$<>i}f zm_68l>`@e)Nuk$+hm=9g-I0$yUz*TFpqtDAx1RL7Yz5N|NDYDh3|}s9fyf)wpcLd9 zfES*VuV4mLSRH7p(<1lfI2b~lt5f+(P8WM9ve?jj;dxM&Bs!?XT1^xKtknY7$j{Ou zke(CuK)QrUMMGa_SqKEm}H*lc2N!S0#x zn~;L3_G$G`i%4G_UDg5tzD_wvst_w)O=VG9*lXcgTr)NRaMhY>KOct6MOLII8L)h8 z#7%iSL7ue^#~Euv@E*#;@t z;c$T9QtsTdT`=}##&FLi0l11l^5S>@YQ&7qm#ezRB50l0vXaF9IS;xfT=KA#!ysj!`pLWFsQ3eG03ifpQtBbQtnlg6&+gz z;@!;VXyy*C-2opB!?6>^82w5fLIO3idu_E<2#Xv0i!xPJWeHXr_zoyXa_&Ow7(Yb@ zfm{K1&m;v~-KI|zVkwKsFR~)OV+eN4ia{73thsqN0O@Dv^D7a*=qzZggRH@>gf~>t zX0c9|(d%*N;f<}4l!~_FgofyS?o36`b zrDyZKcosusb8x+4Ni%TsK4|?f^^oBPbML+qDe9>r6q(5*DzYCZwDr<%ftI$n-CGIMWt2Q+`T%Bh)MmgVhVPB5xS`qXp%bVuqz`LLH_jl zyJp|NKA>v=&N#(`7i9>P&r%bo6meWX19H-$oXy;2bjrwQA#WsvxlZV{nma25Dp~xC`K5O>$e>|dK*J6-A7Mg8n8|ROAGUIpv zYzSmmz}@Hmj^8d;Kp&*Vqyz)zr$kf@DLTEjwl2E#tTUG@kh=Ojj{jbYaJ>(#D*XQd zEJ_8y6m1Bysl?7d&up6+i+&xPgDnK)Ry{rmFm)Q*P5YJSwo6i@(4kdeovNklDCHt6 z`TA!X-_66NLMuXpHBPun4w$$RHj8}fXOAmbdN!p|G%!TDG69-Bq^T3oEk@uZz?3EY z^Oq9zc{rpBZ0&)}X%*MKe#__AkoEQ@o>fpBP^yZE<04L8DCapY{TuI~dFacxJN_(u z)W20#aE;)nWhgDTqpZ?_r+S1TCoLLr5x&*U-}O^YT>|3Y(W>@8BadDD&L!+RTENr9 zpa69(;l^^ZCL?bjU~U;zk)Kkom~dZYrbJ>&QaFbr4R%YrLETt*zQ=D>5F>T&$i0T; zf4+iqS1PLKI;iXjH|c;GM*Mi&6nvwa%PZ}?yZdOHAY_kjh5=pl)oSLOKv$(31m1z` zLG0KMX5{Roz)~yGJrJQIC6!+<)u){r&TO-se5%Ip@5e^PKZ}-}iY=!a0nkIOqTf004+voi(!; zXqP`*Ohh2Z=K7ojY8URb?P&nuSsHl5Q&?ah^ge5E3jl<}0Dve80I()-Mg0H(LbL#Y zX%7GZMFRk!frT&58wmm;UT8}*!1kYYm-!$I007%rnVohZ4*e>);ovYS8`pH|!wHKl zm|qF(I@-%+ED;1c;x7~Sp4O_Sc{&9w1`@kv;!@JDQHQ_UmsNE9fo)HRf!}SjePi~+ zFavx^yjXkB>)q>`T3x$&Ki6GlB$niTqgLj{GaDw26h9iytw+!!=JZy2%AGcUS}@MX zIo<3N(VN~Qbog3esl>Bsij@DyLzfimSt+R2#Lw_Qz73kNz?ZA=ca&U6Jl1kbRIhUn zI*rD)Q1Ak7eiO*w+;Bl6;%pu2#~WoUlF}K=Q;=uvnu$Aw7Y^@8{p>wja1(5hMK)Y@ zp^!;%hK8|A+y%PsLER?c{rL%FYT?vJoHX9zqziCce=WLsW8g|_q>qK`8J)Fh7%pjZ z;Bj~7lA83}QH%NnW{Pm@V8x}yrC%E%&cj{uIIq(cwX@1eE|}`28{99W)$dU~1hu^j zlAoelZVlX~$MPKH;6Nis2oiB$MBXO@t+%$7IS&WA%diE<2;PsHJvqhG^kkm!Gr=J) z)Ky%fI{qqGcd47W?X{D$Hj-T5)EgJps9pNBq@$R(8b_>D_3CLp7D$tGz2`zakKNZ_ zSRTC+eT;859Yc1}&0IL3AXeX(`I?~S38dTd_d@r50=q|4i!`1m2@T0ROg0ePSlf5J z?o~8keRFw(=X1VlO?s#S>yJRUHaf1NbZ!j(;zN?YRTP5L{0H+sp!xQH2`WNU=;im- zsClEQMuA(`?HS$0flVRe}QDZx&K4dDZo&L!?7Z`7=i%5lHnCdHGk8*<-AbCDu;2s@ASRx8-FGeTV1* z5m8naJXm*OT}<|NjK+LQ5+#EAySg41nM~jsXKWa=)L-qE_chPX;E9a092F{}a59Jp zIR~7ZO~V=Pwknq+%4brpz4i2EgmbB?r1AHlt^s$De`JDDAfeRokWmTN9fROn6&nz# zIp0XP!nx|Hx!!BWZlQ+rue(n=O*rvw!M@amhy>Xu;x8?M3fG}xRR_72ok+Ypkhtnh z!-Q-c=U$3q;bQB%x~9~8bR@K+!nr9<7}iNs0fFs1^$tJUZzVx-Zx}Rj)M?72NnhD5 zQQf|}t0sucmdI;Xp$M21MH*&OCQ;I}R)?YWdAz^y1Fk+9RaFn6jf-{KHsV}tK+H7X zB3EC^*qh95EOhg_dUmfq14BG|e`frjm|~#DhLV+dur9PF0yy~iWqb6=@_2O$A_4K{ z``1}q6G4I=?={Ife5LeqKS_gLVw{E`7Ln}HU# zKTa8`lT4Wm*4UUAuL;#9L?k5a7hGIWk2#Az+RgU2RMf9}Y6|hceuUKNCP?F%29$@1 zMg+9pqM0TScd$7{vg-{}Ni<*o+hs^(zZ9z!jJ$N1JOA2N zGDO+vy^SM*-KD`D3!R8*;k&M?zQQ!{R^JQDEF43;yz{KJb`OsAv##QXMopd!y4avy z8Am>cAaR;^By*bo=^$;73Z%wRIx>TD>Se(fbn*>dN%^-B_ZKGok@6kMLp=f-7E_Ig zGrHei?BmW~Rji5mjBfXpf6Fjqq)-ZYU7rNFn11th{DYl;ULtB9(D_0EujE$w{vJIY2aMb6OxUyX zIcSNcGC$=p8CCGbxFz<2xt23(t17MS)WhE8v1N$eg})?Jo`3fklcQ=m7vz9e)VOy3 zf;U$XJ-sop&?&Kg8Q(2tft9B6c)48zxW@hBEQF|LTIqC>Kilyp#AZy)8Sp7|(xZzwsO0cpW{ z!-9CS#Uf<|8q<_HJ&yrckLXbAG{Xzy&?qx!&_fDv>n9Ji{?d;Ka z(^geYw>?AWv326$(gj|t#Z7su)?QoLUTU7sSrZY?hI0diTU6YjTJqCi{O`AOWqPaI z`efAB=3wIU!Nqyi*xJOtm^MF}Uj!jNd-$#iF}1&{V$}lAX3-HB4P5HSbA4+)V6MVx z0ln|i%#5BP6~eWNH&M^&nkG*g9$Hf0iFGu4%c1D#*>xKa*cP!7^4y~B`S?B@FJBoi z&D@M{I~vasFoy{^AvFZQXa}S#W`3l>fuC@fY`Wsb(T zohUmUAW~)5!gtiUe)QS%IEu61ROj2fOFcEaUGgkehA#7G)(<>j$B#7HK5Y4Z^0G`V zhKzt58eiPC)BS}o{b|wVce>nWxP2j(rj%1*HPaQU-Xug&A=G_d%7!BU8igJ&V$gpL3oAy)WYn$+EH`yPjII$4*ONu=ov{%Zq6s3&QiQnHi?2_*| zgUN_g@|Q(Fp;@zA?*z?kS_u1o{rCV|UiouGF3k8-1ee&jJQV#i+=>4>uiQ#1>J^Z4=R_}wUsW`Y%s~esmq-AgYQDVm#zK|>eB2DU*4P0^|(y(cUF*!w{)|KKBlOQbSuthWIC{= zjc=hpyG71PAb^!amU$>aOgP@W|i1A)j&!36zibu~d>v@}r@*nqb7k zYHpZ&_My@wCDAXH0W3Lst{4Bl13X6E7b=&3_uau*cA4Ic zBj>(h2GnP+;EIT2m!=QFasKsmLGAHc4PwUhasaYbKsVBuoc{o#wTm){^94;Sn407#FE5ZEv4mY0*A=LS>i zMjJo%UfFD>g2FfK+M=RzhnQH*Z!y%iuUta_R#~L#Wdi=eLPq+b&^zK@W>wrDU1vLzg4) z0e8^5opRzi*5r~eiDpK1YQh=hV;z_ ze?bQBakjyH&VEYc->p>20T=66De)_|GBt4Xbwk@_SFI#Qz<9_Ae9`&3!ir`SW~D}t zr*(*&OAgiRJ~*2dJ*T}^7o_J4-MpmwS}=?<+55g)D13vmE1Lz@yF9lT!8A&TGeLsk zbw#7$U$wQ@|KsmdiuVb{=>{2(1jDvku(?yPk7qCzwl-YL0Isd=fJCB> zpFrv8!nJfzT3Xi91Bw3u-~)X8d~f^*aQqKITmN4`l5BN^006Ku$Cy=`dffUS2a5G( literal 0 HcmV?d00001 diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..9d947cd9b --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,138 @@ +{ + "images" : [ + { + "filename" : "48.png", + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "24x24", + "subtype" : "38mm" + }, + { + "filename" : "55.png", + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "27.5x27.5", + "subtype" : "42mm" + }, + { + "filename" : "Icon-59.png", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "Icon-87.png", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "66.png", + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "33x33", + "subtype" : "45mm" + }, + { + "filename" : "Icon-80.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "40x40", + "subtype" : "38mm" + }, + { + "filename" : "88.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "44x44", + "subtype" : "40mm" + }, + { + "filename" : "92.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "46x46", + "subtype" : "41mm" + }, + { + "filename" : "100.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "50x50", + "subtype" : "44mm" + }, + { + "filename" : "102.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "51x51", + "subtype" : "45mm" + }, + { + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "54x54", + "subtype" : "49mm" + }, + { + "filename" : "172.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "86x86", + "subtype" : "38mm" + }, + { + "filename" : "196.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "98x98", + "subtype" : "42mm" + }, + { + "filename" : "216.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "108x108", + "subtype" : "44mm" + }, + { + "filename" : "234.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "117x117", + "subtype" : "45mm" + }, + { + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "129x129", + "subtype" : "49mm" + }, + { + "filename" : "Icon-1024.png", + "idiom" : "watch-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-1024.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-1024.png new file mode 100644 index 0000000000000000000000000000000000000000..22db2584c848dd725d1afd135abf60af3fa2cce6 GIT binary patch literal 17162 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 literal 0 HcmV?d00001 diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-59.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-59.png new file mode 100644 index 0000000000000000000000000000000000000000..2f3bf8af1b2e923c0b4b583af35fdc6792019087 GIT binary patch literal 1157 zcmV;01bX|4P)DWfs8V-XQ#V}45Yqe&I%fdM47rEs zt4{>6eX~{(2z~dPf!I$v_jrXY0*|c-0MB)36+^}C4{4~?0N(hysjI30W0kY!7=WNg zNau)`x|}mtsAloT&yA=hG11R^=Y)M8dqBnd*1)`&2;7Y16(gCu&{yYj!ajGk)!Gsz z0;rM`b+mU=>3O&l^@F-j%1$bgsUUUX1ENv@y-8i!Vg!E?U5L8;h;nk0BxXt$$wBmH zF(6rtm_+K56uuK&5F?qV9A{#La>dB|M4in=~_V9fnG}IJx_xzPPp*fVog66Tli&?1~J+JM3ve7ojuR~d(IfSU&HHZW_SyKp6 z?w3pj`=U3AWX3G}&ggd5Sfe2JMih~GYMaVhh@HIw(YTeunU2>c(|LS!CQ&wZZ)Kq% z!-;)SY-kJGe2;vSz{GSOqH!yt3P=R>45V=O!i+1b8>tWA^gFAt`Q93vU;j`RU-nPK zNzB5C7VOQFWB7AqPAZ~n?o{#Z`>XNbhLFwYuYYyCc03N#au%;5D)j}MnKkj!XQLP% z*QJQjwvKxIa9}Ogwc6=AGGSoPu~A_GDi$M$jzom`q=6lWMsRgZcbsVL3WlSvwBp3; z?P#dA7qtIeH?Z^22u2e+%8Au6bqf}sGO+EyFwR}h2vhfw`|I$_;dOX%Yq;dN^H*l^ z?1#g+me5gdoKpIXrqU+5k6y$6&KB%@X60?iw?{PW?`lEkmc|9g^bAbn?bv_F>2gj@ ztfl+1)R*?Vw}x@#C4B(RlTL$0)8Fb3>-Jc5I}qqdXB{3oYd_@rk7$G=a&wA|?m zVNIPEUF|oCHfeb8`Brp4+60B!FSmLIlKA+{7)BF@n_+;3cMf{eShI0$2=DHS;;Bt{ zfC1?JJ%z(xkGn$Cl8H9%81%G*C>qhAFd#l@c=@y5_v^+db@=gA+gO)eqSe2HEV1W* XN;65Y?9lWr00000NkvXXu0mjfeMUqy literal 0 HcmV?d00001 diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-80.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-80.png new file mode 100644 index 0000000000000000000000000000000000000000..01e5a4ae47000ce78ca4d0f6bb29a06a74cc522a GIT binary patch literal 1419 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 zUC08M$52(78v183" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "auto-scaling" : "auto" + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json new file mode 100644 index 000000000..e8b3252e3 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json @@ -0,0 +1,53 @@ +{ + "assets" : [ + { + "filename" : "Circular.imageset", + "idiom" : "watch", + "role" : "circular" + }, + { + "filename" : "Extra Large.imageset", + "idiom" : "watch", + "role" : "extra-large" + }, + { + "filename" : "Graphic Bezel.imageset", + "idiom" : "watch", + "role" : "graphic-bezel" + }, + { + "filename" : "Graphic Circular.imageset", + "idiom" : "watch", + "role" : "graphic-circular" + }, + { + "filename" : "Graphic Corner.imageset", + "idiom" : "watch", + "role" : "graphic-corner" + }, + { + "filename" : "Graphic Extra Large.imageset", + "idiom" : "watch", + "role" : "graphic-extra-large" + }, + { + "filename" : "Graphic Large Rectangular.imageset", + "idiom" : "watch", + "role" : "graphic-large-rectangular" + }, + { + "filename" : "Modular.imageset", + "idiom" : "watch", + "role" : "modular" + }, + { + "filename" : "Utilitarian.imageset", + "idiom" : "watch", + "role" : "utilitarian" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json new file mode 100644 index 000000000..26454cac8 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : "<=145" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "auto-scaling" : "auto" + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json new file mode 100644 index 000000000..6e184db8f --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "auto-scaling" : "auto" + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json new file mode 100644 index 000000000..6e184db8f --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "auto-scaling" : "auto" + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json new file mode 100644 index 000000000..6e184db8f --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "auto-scaling" : "auto" + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json new file mode 100644 index 000000000..26454cac8 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : "<=145" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "auto-scaling" : "auto" + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json new file mode 100644 index 000000000..6e184db8f --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "auto-scaling" : "auto" + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json new file mode 100644 index 000000000..26454cac8 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : "<=145" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "auto-scaling" : "auto" + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json new file mode 100644 index 000000000..26454cac8 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : "<=145" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "auto-scaling" : "auto" + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DarkTextMuted.colorset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DarkTextMuted.colorset/Contents.json new file mode 100644 index 000000000..d5dac6ad3 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DarkTextMuted.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xCE", + "green" : "0xC0", + "red" : "0xBA" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/Contents.json new file mode 100644 index 000000000..a6254502d --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "globe 1.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "globe.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "globe 2.svg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe 1.svg b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe 1.svg new file mode 100644 index 000000000..f0f5ed288 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe 1.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe 2.svg b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe 2.svg new file mode 100644 index 000000000..f0f5ed288 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe 2.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe.svg b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe.svg new file mode 100644 index 000000000..f0f5ed288 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/Contents.json new file mode 100644 index 000000000..0d301e528 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "emptystatedark.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "emptystatedark 1.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "emptystatedark 2.svg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark 1.svg b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark 1.svg new file mode 100644 index 000000000..f0c83d064 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark 1.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark 2.svg b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark 2.svg new file mode 100644 index 000000000..f0c83d064 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark 2.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark.svg b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark.svg new file mode 100644 index 000000000..f0c83d064 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/ItemBackground.colorset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/ItemBackground.colorset/Contents.json new file mode 100644 index 000000000..cbecde52e --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/ItemBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "38", + "green" : "28", + "red" : "22" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/ComplicationController.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/ComplicationController.swift new file mode 100644 index 000000000..c369727a6 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/ComplicationController.swift @@ -0,0 +1,52 @@ +import ClockKit + + +class ComplicationController: NSObject, CLKComplicationDataSource { + + // MARK: - Complication Configuration + + func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) { + let descriptors = [ + CLKComplicationDescriptor(identifier: "complication", displayName: "bitwarden", supportedFamilies: CLKComplicationFamily.allCases) + // Multiple complication support can be added here with more descriptors + ] + + // Call the handler with the currently supported complication descriptors + handler(descriptors) + } + + func handleSharedComplicationDescriptors(_ complicationDescriptors: [CLKComplicationDescriptor]) { + // Do any necessary work to support these newly shared complication descriptors + } + + // MARK: - Timeline Configuration + + func getTimelineEndDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) { + // Call the handler with the last entry date you can currently provide or nil if you can't support future timelines + handler(nil) + } + + func getPrivacyBehavior(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationPrivacyBehavior) -> Void) { + // Call the handler with your desired behavior when the device is locked + handler(.showOnLockScreen) + } + + // MARK: - Timeline Population + + func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) { + // Call the handler with the current timeline entry + handler(nil) + } + + func getTimelineEntries(for complication: CLKComplication, after date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) { + // Call the handler with the timeline entries after the given date + handler(nil) + } + + // MARK: - Sample Templates + + func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) { + // This method will be called once per supported complication, and the results will be cached + handler(nil) + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/AvatarView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/AvatarView.swift new file mode 100644 index 000000000..e31bc6139 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/AvatarView.swift @@ -0,0 +1,85 @@ +import SwiftUI + +struct AvatarView: View { + var circleColor = Color.white + var textColor = Color.black + var initials = "" + + init(_ user: User?) { + let source = user?.name ?? user?.email + var upperCaseText: String? = nil + + if source == nil || source!.isEmpty { + initials = ".." + } else if source!.count > 1 { + upperCaseText = source!.uppercased() + initials = getFirstLetters(upperCaseText!, 2) + } else { + upperCaseText = source!.uppercased() + initials = upperCaseText! + } + + circleColor = stringToColor(str: user?.id ?? upperCaseText, fallbackColor: Color(hex: "#FFFFFF33")!) + textColor = textColorFromBgColor(circleColor) + } + + var body: some View { + ZStack { + Circle() + .foregroundColor(circleColor) + .frame(width: 30, height: 30) + Text(initials) + .font(.footnote) + .foregroundColor(textColor) + } + } + + func stringToColor(str: String?, fallbackColor: Color) -> Color { + guard let str = str else { + return fallbackColor + } + + var hash = 0 + for char in str { + let uniSca = String(char).unicodeScalars + let intCharValue = Int(uniSca[uniSca.startIndex].value) + + hash = intCharValue + ((hash << 5) &- hash) + } + var color = "#" + for i in 0..<3 { + let value = (hash >> (i * 8)) & 0xff + color += String(value, radix: 16).leftPadding(toLength: 2, withPad: "0") + } + return Color(hex: color) ?? fallbackColor + } + + func textColorFromBgColor(_ bgColor: Color, threshold: CGFloat = 0.65) -> Color { + let (r, g, b, _) = bgColor.components + let luminance = r * 0.299 + g * 0.587 + b * 0.114; + return luminance > threshold ? Color.black : Color.white; + } + + func getFirstLetters(_ data: String, _ charCount: Int) -> String { + let sanitizedData = data.trimmingCharacters(in: CharacterSet.whitespaces) + let parts = sanitizedData.split(separator: " ") + + if parts.count > 1 && charCount <= 2 { + var text = ""; + for i in 0.. 2 { + return String(sanitizedData.prefix(2)) + } + return sanitizedData; + } +} + +struct AvatarView_Previews: PreviewProvider { + static var previews: some View { + AvatarView(User(id: "zxc", email: "asdfasdf@gmail.com", name: "John Snow")) + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/CircularProgressView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/CircularProgressView.swift new file mode 100644 index 000000000..791e7a73d --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/CircularProgressView.swift @@ -0,0 +1,39 @@ +import SwiftUI + +struct CircularProgressView: View { + let progress: Double + let strokeLineWidth:CGFloat + let strokeColor:Color + let endingStrokeColor:Color + + var currentColor: Color{ + return progress > 0.2 ? strokeColor : endingStrokeColor + } + + var body: some View { + ZStack { + Circle() + .stroke( + currentColor.opacity(0.5), + lineWidth: strokeLineWidth + ) + Circle() + .trim(from: 0, to: progress) + .stroke( + currentColor, + style: StrokeStyle( + lineWidth: strokeLineWidth, + lineCap: .round + ) + ) + .rotationEffect(.degrees(-90)) + .animation(.easeOut, value: progress) + } + } +} + +struct CircularProgressView_Previews: PreviewProvider { + static var previews: some View { + CircularProgressView(progress:0.5, strokeLineWidth:5, strokeColor: Color.blue, endingStrokeColor: Color.red) + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/ImageView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/ImageView.swift new file mode 100644 index 000000000..05467f330 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/ImageView.swift @@ -0,0 +1,112 @@ +import Foundation +import SwiftUI +import Combine + +/// Image view to be used on watchOS < 8 +/// +/// - Note: Based on: https://stackoverflow.com/questions/60710997/images-disappear-in-list-as-i-scroll-swiftui-swift +/// +struct ImageView: View { + @ObservedObject var imageLoader:ImageLoader + var imgMaxWidth:CGFloat + var imgMaxHeight:CGFloat + var placeholder: PlaceholderView + + init(withURL url:String, maxWidth mw: CGFloat, maxHeight mh: CGFloat, @ViewBuilder _ placeholder: () -> PlaceholderView) { + imageLoader = ImageLoader(urlString:url) + self.imgMaxWidth = mw + self.imgMaxHeight = mh + self.placeholder = placeholder() + } + + var body: some View { + if imageLoader.image == nil { + placeholder + } else { + Image(uiImage: imageLoader.image! ) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth:imgMaxWidth, maxHeight:imgMaxHeight) + } + } +} + +class ImageLoader: ObservableObject { + @Published var image: UIImage? + var urlString: String? + var imageCache = ImageCache.getImageCache() + + init(urlString: String?) { + self.urlString = urlString + loadImage() + } + + func loadImage() { + if loadImageFromCache() { + return + } + + loadImageFromUrl() + } + + func loadImageFromCache() -> Bool { + guard let urlString = urlString else { + return false + } + + guard let cacheImage = imageCache.get(forKey: urlString) else { + return false + } + + image = cacheImage + return true + } + + func loadImageFromUrl() { + guard let urlString = urlString else { + return + } + + let url = URL(string: urlString)! + let task = URLSession.shared.dataTask(with: url, completionHandler: getImageFromResponse(data:response:error:)) + task.resume() + } + + + func getImageFromResponse(data: Data?, response: URLResponse?, error: Error?) { + guard error == nil else { + return + } + guard let data = data else { + return + } + + DispatchQueue.main.async { + guard let loadedImage = UIImage(data: data) else { + return + } + + self.imageCache.set(forKey: self.urlString!, image: loadedImage) + self.image = loadedImage + } + } +} + +class ImageCache { + var cache = NSCache() + + func get(forKey: String) -> UIImage? { + return cache.object(forKey: NSString(string: forKey)) + } + + func set(forKey: String, image: UIImage) { + cache.setObject(image, forKey: NSString(string: forKey)) + } +} + +extension ImageCache { + private static var imageCache = ImageCache() + static func getImageCache() -> ImageCache { + return imageCache + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/TrackableWithHeaderListView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/TrackableWithHeaderListView.swift new file mode 100644 index 000000000..f6c1a147e --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/TrackableWithHeaderListView.swift @@ -0,0 +1,40 @@ +import Foundation +import SwiftUI + +/// List that has offset tracking and a header +/// +/// - Note: Based on: https://stackoverflow.com/questions/74047146/tracking-scroll-position-in-a-list-swiftui +/// +struct TrackableWithHeaderListView: View { + let offsetChanged: (CGPoint?) -> Void + let headerContent: HeaderContent + let content: Content + + init(offsetChanged: @escaping (CGPoint?) -> Void = { _ in }, @ViewBuilder headerContent: () -> HeaderContent, @ViewBuilder content: () -> Content) { + self.offsetChanged = offsetChanged + self.headerContent = headerContent() + self.content = content() + } + + var body: some View { + List { + GeometryReader { geometry in + headerContent + .preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("ListView")).origin) + } + .frame(width: .infinity) + content + } + .coordinateSpace(name: "ListView") + .onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: offsetChanged) + } +} + +private struct ScrollOffsetPreferenceKey: PreferenceKey { + static var defaultValue: CGPoint? = nil + static func reduce(value: inout CGPoint?, nextValue: () -> CGPoint?) { + if let nextValue = nextValue() { + value = nextValue + } + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/DataStorage/CoreDataHelper.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/DataStorage/CoreDataHelper.swift new file mode 100644 index 000000000..3c51d2524 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/DataStorage/CoreDataHelper.swift @@ -0,0 +1,127 @@ +import Foundation +import CoreData + +// Based on https://medium.com/swlh/using-core-data-in-your-swiftui-app-with-combine-mvvm-and-protocols-4577f44d240d + +class CoreDataHelper: DBHelperProtocol { + static let shared = CoreDataHelper() + + typealias ObjectType = NSManagedObject + typealias PredicateType = NSPredicate + + var context: NSManagedObjectContext { persistentContainer.viewContext } + + // MARK: - Core Data + + lazy var persistentContainer: NSPersistentContainer = { + StringEncryptionTransformer.register() + let container = NSPersistentContainer(name: "BitwardenDB") + + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + return container + }() + + func saveContext () { + let context = persistentContainer.viewContext + if context.hasChanges { + do { + try context.save() + } catch { + let nserror = error as NSError + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } + } + } + + // MARK: - DBHelper Protocol + + + func create(_ object: NSManagedObject) { + do { + try context.save() + } catch { + fatalError("error saving context while creating an object") + } + } + + func fetch(_ objectType: T.Type, _ entityName: String, predicate: NSPredicate? = nil, limit: Int? = nil) -> Result<[T], Error> { + let request = NSFetchRequest(entityName: entityName) + request.predicate = predicate + if let limit = limit { + request.fetchLimit = limit + } + do { + let result = try context.fetch(request) + return .success(result as [T]) + } catch { + return .failure(error) + } + } + + func fetchFirst(_ objectType: T.Type, predicate: NSPredicate?) -> Result { + let result = fetch(objectType, predicate: predicate, limit: 1) + switch result { + case .success(let todos): + return .success(todos.first as? T) + case .failure(let error): + return .failure(error) + } + } + + func update(_ object: NSManagedObject) { + do { + try context.save() + } catch { + fatalError("error saving context while updating an object") + } + } + + func delete(_ object: NSManagedObject) { + context.delete(object) + } + + func insertBatch(_ entityName: String, items: [Any], itemMapper: @escaping (Any, NSManagedObjectContext) -> [String : Any], completionHandler: @escaping () -> Void) { + self.persistentContainer.performBackgroundTask { context in + context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + let objects = items.map { item in + itemMapper(item, context) + } + let batchInsert = NSBatchInsertRequest(entityName: entityName, objects: objects) + batchInsert.resultType = NSBatchInsertRequestResultType.objectIDs + do { + let result = try context.execute(batchInsert) as! NSBatchInsertResult + if let objectIDs = result.result as? [NSManagedObjectID], !objectIDs.isEmpty { + let save = [NSInsertedObjectsKey: objectIDs] + NSManagedObjectContext.mergeChanges(fromRemoteContextSave: save, into: [self.context]) + } + } + catch let nsError as NSError { + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + DispatchQueue.main.async { + completionHandler() + } + } + } + + func deleteAll(_ entityName: String, predicate: NSPredicate? = nil, completionHandler: @escaping () -> Void) { + let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: entityName) + fetchRequest.predicate = predicate + let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + deleteRequest.resultType = .resultTypeObjectIDs + + self.persistentContainer.performBackgroundTask { context in + do { + try context.execute(deleteRequest) + } catch let nsError as NSError { + Log.e("Unresolved error \(nsError), \(nsError.userInfo)") + } + + completionHandler() + } + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/DataStorage/DBHelperProtocol.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/DataStorage/DBHelperProtocol.swift new file mode 100644 index 000000000..db482194b --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/DataStorage/DBHelperProtocol.swift @@ -0,0 +1,20 @@ +import Foundation +import CoreData + +public protocol DBHelperProtocol { + associatedtype ObjectType + associatedtype PredicateType + + func create(_ object: ObjectType) + func fetchFirst(_ objectType: ObjectType.Type, predicate: PredicateType?) -> Result + func fetch(_ objectType: ObjectType.Type, predicate: PredicateType?, limit: Int?) -> Result<[ObjectType], Error> + func update(_ object: ObjectType) + func delete(_ object: ObjectType) + func insertBatch(_ entityName: String, items: [Any], itemMapper: @escaping (Any, NSManagedObjectContext) -> [String : Any], completionHandler: @escaping () -> Void) +} + +public extension DBHelperProtocol { + func fetch(_ objectType: ObjectType.Type, predicate: PredicateType? = nil, limit: Int? = nil) -> Result<[ObjectType], Error> { + return fetch(objectType, predicate: predicate, limit: limit) + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/BitwardenDB.xcdatamodeld/BitwardenDB.xcdatamodel/contents b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/BitwardenDB.xcdatamodeld/BitwardenDB.xcdatamodel/contents new file mode 100644 index 000000000..137e02a5c --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/BitwardenDB.xcdatamodeld/BitwardenDB.xcdatamodel/contents @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/CipherEntity+CoreDataClass.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/CipherEntity+CoreDataClass.swift new file mode 100644 index 000000000..0631111a6 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/CipherEntity+CoreDataClass.swift @@ -0,0 +1,43 @@ +import Foundation +import CoreData + +enum DecoderConfigurationError: Error { + case missingManagedObjectContext +} + +@objc(CipherEntity) +public class CipherEntity: NSManagedObject, Codable { + enum CodingKeys: CodingKey { + case id, name, username, totp, loginUris, userId + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(userId, forKey: .userId) + try container.encode(username, forKey: .username) + try container.encode(totp, forKey: .totp) + try container.encode(loginUris, forKey: .loginUris) + } + + public required convenience init(from decoder: Decoder) throws { + guard let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext else { + throw DecoderConfigurationError.missingManagedObjectContext + } + + self.init(context: context) + + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(String.self, forKey: .id) + self.name = try container.decode(String?.self, forKey: .name) + self.userId = try container.decode(String.self, forKey: .userId) + self.username = try container.decode(String?.self, forKey: .username) + self.totp = try container.decode(String?.self, forKey: .totp) + self.loginUris = try container.decode(String?.self, forKey: .loginUris) + } +} + +extension CodingUserInfoKey { + static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")! +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/CipherEntity+CoreDataProperties.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/CipherEntity+CoreDataProperties.swift new file mode 100644 index 000000000..91bfd2d1f --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/CipherEntity+CoreDataProperties.swift @@ -0,0 +1,34 @@ +import Foundation +import CoreData + + +extension CipherEntity { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "CipherEntity") + } + + @NSManaged public var id: String + @NSManaged public var name: String? + @NSManaged public var userId: String + @NSManaged public var totp: String? + @NSManaged public var type: NSObject? + @NSManaged public var username: String? + @NSManaged public var loginUris: String? + +} + +extension CipherEntity : Identifiable { + func toCipher() -> Cipher{ + + var loginUrisArray: [LoginUri]? + if loginUris != nil { + loginUrisArray = try? JSONDecoder().decode([LoginUri].self, from: loginUris!.data(using: .utf8)!) + } + + return Cipher(id: id, + name: name, + userId: userId, + login: Login(username: username, totp: totp, uris: loginUrisArray)) + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/StringEncryptionTransformer.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/StringEncryptionTransformer.swift new file mode 100644 index 000000000..3fae2d5e7 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/StringEncryptionTransformer.swift @@ -0,0 +1,48 @@ +import Foundation +import UIKit + +@objc(StringEncryptionTransformer) +class StringEncryptionTransformer : ValueTransformer { + var cryptoService: CryptoService = CryptoService() + + override public class func allowsReverseTransformation() -> Bool { + return true + } + + override func transformedValue(_ value: Any?) -> Any?{ + var toEncrypt: String + + switch value { + case let aString as String: + toEncrypt = aString + default: + return nil + } + + if let encryptedData = cryptoService.encrypt(toEncrypt) { + return encryptedData + } + + return nil + } + + override func reverseTransformedValue(_ value: Any?) -> Any?{ + if let encryptedData = value as? Data { + if let decryptedData = cryptoService.decrypt(encryptedData) { + return String(decoding: decryptedData, as: UTF8.self) + } + } + + return nil + } +} + +extension StringEncryptionTransformer { + static let name = NSValueTransformerName(rawValue: String(describing: StringEncryptionTransformer.self)) + + /// Registers the value transformer with `ValueTransformer`. + public static func register() { + let transformer = StringEncryptionTransformer() + ValueTransformer.setValueTransformer(transformer, forName: name) + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/IconImageHelper.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/IconImageHelper.swift new file mode 100644 index 000000000..f25daeb03 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/IconImageHelper.swift @@ -0,0 +1,38 @@ +import Foundation + +class IconImageHelper{ + static let shared: IconImageHelper = IconImageHelper() + + private init(){} + + func getLoginIconImage(_ cipher:Cipher) -> String? { + guard let uris = cipher.login.uris, uris.count > 0 else { + return nil + } + + for u in uris { + guard var hostname = u.uri, hostname.contains(".") else { + continue + } + + if !hostname.contains("://") { + hostname = "http://\(hostname)" + } + + if hostname.starts(with: "http") { + return getIconUrl(hostname) + } + } + + return nil + } + + func getIconUrl(_ uriString:String?) -> String? { + guard let uriString = uriString else { + return nil + } + + let hostname = URL.createFullUri(from: uriString)?.host + return hostname == nil ? "\(EnvironmentService.shared.iconsUrl)/icon.png" : "\(EnvironmentService.shared.iconsUrl)/\(hostname!)/icon.png" + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/KeychainHelper.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/KeychainHelper.swift new file mode 100644 index 000000000..e23a787a9 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/KeychainHelper.swift @@ -0,0 +1,94 @@ +import Foundation + +final class KeychainHelper { + + static let standard = KeychainHelper() + let genericService = "com.8bit.bitwarden.watch.kc" + + private init() {} + + func read(_ key: String, _ type: T.Type) -> T? where T : Codable { + guard let data = read(key) else { + return nil + } + + do { + let item = try JSONDecoder().decode(type, from: data) + return item + } catch { + assertionFailure("Fail to decode item for keychain: \(error)") + return nil + } + } + + func save(_ item: T, key: String) where T : Codable { + + do { + let data = try JSONEncoder().encode(item) + save(data, key) + + } catch { + assertionFailure("Fail to encode item for keychain: \(error)") + } + } + + // MARK: NON-GENERIC FUNC + func read(_ key: String) -> Data? { + let query = [ + kSecAttrService: genericService, + kSecAttrAccount: key, + kSecClass: kSecClassGenericPassword, + kSecReturnData: true + ] as CFDictionary + + var result: AnyObject? + SecItemCopyMatching(query, &result) + + return (result as? Data) + } + + func save(_ data: Data, _ key: String) { + if let _ = read(key) { + delete(key) + } + + let query = [ + kSecValueData: data, + kSecClass: kSecClassGenericPassword, + kSecAttrService: genericService, + kSecAttrAccount: key, + kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock + ] as CFDictionary + + let status = SecItemAdd(query, nil) + + if status == errSecDuplicateItem { + // Item already exist, thus update it. + let query = [ + kSecAttrService: genericService, + kSecAttrAccount: key, + kSecClass: kSecClassGenericPassword, + ] as CFDictionary + + let attributesToUpdate = [kSecValueData: data] as CFDictionary + + SecItemUpdate(query, attributesToUpdate) + } + + + if status != errSecSuccess { + Log.e("Error: \(status)") + } + } + + func delete(_ key: String) { + + let query = [ + kSecAttrService: genericService, + kSecAttrAccount: key, + kSecClass: kSecClassGenericPassword, + ] as CFDictionary + + SecItemDelete(query) + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/LoggerHelper.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/LoggerHelper.swift new file mode 100644 index 000000000..bcbb8abe4 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/LoggerHelper.swift @@ -0,0 +1,54 @@ +import Foundation + +/// Wraps Swift.print() within DEBUG +/// +/// - Note: *print()* might cause [security vulnerabilities](https://codifiedsecurity.com/mobile-app-security-testing-checklist-ios/) +/// +/// - Parameter object: The object which is to be logged +/// +func print(_ object: Any) { + #if DEBUG + Swift.print(object) + #endif +} + +class Log{ + + static let shared = Log() + + private init() {} + + private static var isLoggingEnabled: Bool { + #if DEBUG + return true + #else + return false + #endif + } + + static var dateFormat = "yyyy-MM-dd hh:mm:ssSSS" + static var dateFormatter: DateFormatter { + let formatter = DateFormatter() + formatter.dateFormat = dateFormat + formatter.locale = Locale.current + formatter.timeZone = TimeZone.current + return formatter + } + + class func e( _ object: Any, filename: String = #file, line: Int = #line, column: Int = #column, funcName: String = #function) { + if isLoggingEnabled { + print("\(Date().toString()) Error [\(sourceFileName(filePath: filename))]:\(line) \(column) \(funcName) -> \(object)") + } + } + + private class func sourceFileName(filePath: String) -> String { + let components = filePath.components(separatedBy: "/") + return components.isEmpty ? "" : components.last! + } +} + +internal extension Date { + func toString() -> String { + return Log.dateFormatter.string(from: self as Date) + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Info.plist b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Info.plist new file mode 100644 index 000000000..17ed31521 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Info.plist @@ -0,0 +1,16 @@ + + + + + NSExtension + + NSExtensionAttributes + + WKAppBundleIdentifier + com.8bit.bitwarden.watchkitapp + + NSExtensionPointIdentifier + com.apple.watchkit + + + diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/en.lproj/Localizable.strings b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/en.lproj/Localizable.strings new file mode 100644 index 000000000..8def799f6 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/en.lproj/Localizable.strings @@ -0,0 +1,9 @@ +"ThereAreNoItemsToList"="There are no items to list"; +"ToViewVerificationCodesUpgradeToPremium"="To view verification codes, upgrade to premium"; +"Add2FactorAutenticationToAnItemToViewVerificationCodes"="Add 2 factor authentication to an item to view the verification codes"; +"LogInToBitwardenOnYourIPhoneToViewVerificationCodes"="Log in to Bitwarden on your iPhone to view verification codes"; +"SyncingItemsContainingVerificationCodes"="Syncing items containing verification codes"; +"UnlockBitwardenOnYourIPhoneToViewVerificationCodes"="Unlock Bitwarden on your iPhone to view verification codes"; +"SetUpBitwardenToViewItemsContainingVerificationCodes"="Set up Bitwarden to view items containing verification codes"; +"Search"="Search"; +"NoItemsFound"="No items found"; diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/Cipher.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/Cipher.swift new file mode 100644 index 000000000..24232f2a0 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/Cipher.swift @@ -0,0 +1,36 @@ +import Foundation +import CoreData + +struct Cipher:Identifiable,Codable{ + var id:String + var name:String? + var userId:String? + var login:Login +} + +struct Login:Codable{ + var username:String? + var totp:String? + var uris:[LoginUri]? +} + +struct LoginUri:Codable{ + var uri:String? +} + +extension Cipher{ + func toCipherEntity(moContext: NSManagedObjectContext) -> CipherEntity{ + let entity = CipherEntity(context: moContext) + entity.id = id + entity.name = name + entity.userId = userId ?? "unknown" + entity.username = login.username + entity.totp = login.totp + + if let uris = login.uris, let encodedData = try? JSONEncoder().encode(uris) { + entity.loginUris = String(data: encodedData, encoding: .utf8) + } + + return entity + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/Mocks/CipherMock.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/Mocks/CipherMock.swift new file mode 100644 index 000000000..543e6a99e --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/Mocks/CipherMock.swift @@ -0,0 +1,17 @@ +import Foundation + +struct CipherMock { + static let ciphers:[Cipher] = [ + Cipher(id: "0", name: "1933", userId: "123123", login: Login(username: "thisisatest@testing.com", totp: "otpauth://account?period=10&secret=LLLLLLLLLLLLLLLL", uris: cipherLoginUris)), + Cipher(id: "1", name: "GitHub", userId: "123123", login: Login(username: "thisisatest@testing.com", totp: "LLLLLLLLLLLLLLLL", uris: cipherLoginUris)), + Cipher(id: "2", name: "No user", userId: "123123", login: Login(username: nil, totp: "otpauth://account?period=10&digits=8&algorithm=sha256&secret=LLLLLLLLLLLLLLLL", uris: cipherLoginUris)), + Cipher(id: "3", name: "Site 2", userId: "123123", login: Login(username: "longtestemail000000@fastmailasdfasdf.com", totp: "otpauth://account?period=10&digits=7&algorithm=sha512&secret=LLLLLLLLLLLLLLLL", uris: cipherLoginUris)), + Cipher(id: "4", name: "Really long name for a site that is used for a totp", userId: "123123", login: Login(username: "user3", totp: "steam://LLLLLLLLLLLLLLLL", uris: cipherLoginUris)), + Cipher(id: "5", name: "Short", userId: "123123", login: Login(username: "u", totp: "steam://LLLLLLLLLLLLLLLL", uris: cipherLoginUris)) + ] + + static let cipherLoginUris:[LoginUri] = [ + LoginUri(uri: "github.com"), + LoginUri(uri: "example2.com") + ] +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/User.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/User.swift new file mode 100644 index 000000000..7320861e8 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/User.swift @@ -0,0 +1,7 @@ +import Foundation + +struct User : Codable { + var id: String + var email: String? + var name: String? +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/VaultTimeoutAction.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/VaultTimeoutAction.swift new file mode 100644 index 000000000..6c6fc1656 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/VaultTimeoutAction.swift @@ -0,0 +1,6 @@ +//import Foundation +// +//enum VaultTimeoutAction : Int, Codable { +// case lock = 0 +// case logout = 1 +//} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/WatchDTO.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/WatchDTO.swift new file mode 100644 index 000000000..429a54465 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/WatchDTO.swift @@ -0,0 +1,26 @@ +import Foundation + +struct WatchDTO : Codable{ + var state: BWState + var ciphers: [Cipher]? + var userData: User? + var environmentData: EnvironmentUrlDataDto? +// var settingsData: SettingsDataDto? + + init(state: BWState) { + self.state = state + self.ciphers = nil + self.userData = nil + self.environmentData = nil + } +} + +struct EnvironmentUrlDataDto : Codable { + var base: String? + var icons: String? +} + +//struct SettingsDataDto : Codable { +// var vaultTimeoutInMinutes: Int? +// var vaultTimeoutAction: VaultTimeoutAction +//} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/NotificationController.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/NotificationController.swift new file mode 100644 index 000000000..73fceb36b --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/NotificationController.swift @@ -0,0 +1,26 @@ +import WatchKit +import SwiftUI +import UserNotifications + +class NotificationController: WKUserNotificationHostingController { + + override var body: NotificationView { + return NotificationView() + } + + override func willActivate() { + // This method is called when watch view controller is about to be visible to user + super.willActivate() + } + + override func didDeactivate() { + // This method is called when watch view controller is no longer visible + super.didDeactivate() + } + + override func didReceive(_ notification: UNNotification) { + // This method is called when a notification needs to be presented. + // Implement it if you use a dynamic notification interface. + // Populate your dynamic notification interface as quickly as possible. + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/NotificationView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/NotificationView.swift new file mode 100644 index 000000000..63211dd15 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/NotificationView.swift @@ -0,0 +1,13 @@ +import SwiftUI + +struct NotificationView: View { + var body: some View { + Text("Hello, World!") + } +} + +struct NotificationView_Previews: PreviewProvider { + static var previews: some View { + NotificationView() + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Preview Content/Preview Assets.xcassets/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/PushNotificationPayload.apns b/src/watchOS/bitwarden/bitwarden WatchKit Extension/PushNotificationPayload.apns new file mode 100644 index 000000000..c18b00ad9 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/PushNotificationPayload.apns @@ -0,0 +1,20 @@ +{ + "aps": { + "alert": { + "body": "Test message", + "title": "Optional title", + "subtitle": "Optional subtitle" + }, + "category": "myCategory", + "thread-id": "5280" + }, + + "WatchKit Simulator Actions": [ + { + "title": "First Button", + "identifier": "firstButtonAction" + } + ], + + "customKey": "Use this file to define a testing payload for your notifications. The aps dictionary specifies the category, alert text and title. The WatchKit Simulator Actions array can provide info for one or more action buttons in addition to the standard Dismiss button. Any other top level keys are custom payload. If you have multiple such JSON files in your project, you'll be able to select them when choosing to debug the notification interface of your Watch App." +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CipherService.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CipherService.swift new file mode 100644 index 000000000..6328ae40c --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CipherService.swift @@ -0,0 +1,70 @@ +import Foundation +import CoreData + +protocol CipherServiceProtocol { + func getCipher(_ id: String) -> Cipher? + func fetchCiphers(_ withUserId: String?) -> [Cipher] + func saveCiphers(_ ciphers: [Cipher], completionHandler: @escaping () -> Void) + func deleteAll(_ withUserId: String?, completionHandler: @escaping () -> Void) +} + +class CipherService { + static let shared: CipherServiceProtocol = CipherService() + + var dbHelper: CoreDataHelper = CoreDataHelper.shared + + private init() { } + + func getCipher(_ id: String) -> Cipher? { + let predicate = NSPredicate( + format: "id = %@", + id as CVarArg) + let result = dbHelper.fetchFirst(CipherEntity.self, predicate: predicate) + switch result { + case .success(let cipherEntity): + return cipherEntity?.toCipher() + case .failure(_): + return nil + } + } +} + +// MARK: - CipherServiceProtocol +extension CipherService: CipherServiceProtocol { + func fetchCiphers(_ withUserId: String?) -> [Cipher] { + let result: Result<[CipherEntity], Error> = dbHelper.fetch(CipherEntity.self, "CipherEntity", predicate: withUserId == nil ? nil : NSPredicate(format: "userId = %@", withUserId!)) + switch result { + case .success(let success): + return success.map { entity in entity.toCipher() } + case .failure(let error): + fatalError(error.localizedDescription) + } + } + + func saveCiphers(_ ciphers: [Cipher], completionHandler: @escaping () -> Void){ + dbHelper.insertBatch("CipherEntity", items: ciphers) { item, context in + guard let cipher = item as! Cipher? else { return [:] } + let c = cipher.toCipherEntity(moContext: context) + guard let data = try? JSONEncoder().encode(c) else + { + Log.e("Error converting to data") + return [:] + } + + guard let cipherDict = try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any ] else + { + Log.e("Error converting json data to dict") + return [:] + } + return cipherDict + + } completionHandler: { + completionHandler() + } + } + + func deleteAll(_ withUserId: String? = nil, completionHandler: @escaping () -> Void) { + let predicate = withUserId == nil ? nil : NSPredicate(format: "userId = %@", withUserId!) + dbHelper.deleteAll("CipherEntity", predicate: predicate, completionHandler: completionHandler) + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CryptoFunctionService.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CryptoFunctionService.swift new file mode 100644 index 000000000..eb2efa6bc --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CryptoFunctionService.swift @@ -0,0 +1,25 @@ +import Foundation +import CryptoKit + +class CryptoFunctionService{ + static let shared: CryptoFunctionService = CryptoFunctionService() + + enum CryptoHashAlgorithm { + case Sha1, Sha256, Sha512 + } + + enum CryptoError: Error { + case AlgorithmNotImplemented + } + + func hmac(_ data: Data, _ key: SymmetricKey, algorithm alg: CryptoHashAlgorithm) -> Data { + switch alg { + case .Sha1: + return Data(HMAC.authenticationCode(for: data, using: key)) + case .Sha256: + return Data(HMAC.authenticationCode(for: data, using: key)) + case .Sha512: + return Data(HMAC.authenticationCode(for: data, using: key)) + } + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CryptoService.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CryptoService.swift new file mode 100644 index 000000000..1a5f6258d --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CryptoService.swift @@ -0,0 +1,97 @@ +import Foundation +import CryptoKit + +public class CryptoService{ + static let ENCRYPTION_KEY: String = "encryptionKey" + + private(set) var key: SymmetricKey? = nil + + init(){ + key = loadKey() + } + + func encrypt(_ plainText: String?) -> Data? { + guard let plainText = plainText, let key = key else { + return nil + } + + let nonce = randomData(lengthInBytes: 12) + + let plainData = plainText.data(using: .utf8) + let sealedData = try! AES.GCM.seal(plainData!, using: key, nonce: AES.GCM.Nonce(data:nonce)) + return sealedData.combined + } + + func decrypt(_ combinedEncryptedData: Data, _ type: T.Type) -> T? { + guard let key = key else { + return nil + } + + let sealedBox = try! AES.GCM.SealedBox(combined: combinedEncryptedData) + let decryptedData = try! AES.GCM.open(sealedBox, using: key) + + do { + let item = try JSONDecoder().decode(type, from: decryptedData) + return item + } catch { + assertionFailure("Fail to decode item for keychain: \(error)") + return nil + } + } + + func decrypt(_ combinedEncryptedData: Data) -> Data? { + guard let key = key else { + return nil + } + + let sealedBox = try! AES.GCM.SealedBox(combined: combinedEncryptedData) + let decryptedData = try! AES.GCM.open(sealedBox, using: key) + return decryptedData + } + + func loadKey() -> SymmetricKey{ + if let encKey = KeychainHelper.standard.read(CryptoService.ENCRYPTION_KEY) { + return SymmetricKey(data: encKey) + } + + // First time so we need to generate the key + let newKey = SymmetricKey(size: .bits256) + let keyData = newKey.withUnsafeBytes({ body in + return Data(Array(body)) + }) + KeychainHelper.standard.save(keyData, CryptoService.ENCRYPTION_KEY) + return newKey + } + + func randomData(lengthInBytes: Int) -> Data { + var data = Data(count: lengthInBytes) + _ = data.withUnsafeMutableBytes { + SecRandomCopyBytes(kSecRandomDefault, lengthInBytes, $0.baseAddress!) + } + return data + } +} + +public extension Data { + init?(hexString: String) { + let len = hexString.count / 2 + var data = Data(capacity: len) + var i = hexString.startIndex + for _ in 0.. [Cipher] { + return ciphers + } + + func deleteAll(_ withUserId: String?, completionHandler: @escaping () -> Void) { + completionHandler() + } + + func getCipher(_ id: String) -> Cipher? { + return CipherMock.ciphers.first { ci in + ci.id == id + } + } + + func saveCiphers(_ ciphers: [Cipher], completionHandler: @escaping () -> Void) { + } + + private var ciphers = [Cipher]() + + init() { + ciphers = CipherMock.ciphers + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/StateService.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/StateService.swift new file mode 100644 index 000000000..97f008520 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/StateService.swift @@ -0,0 +1,73 @@ +import Foundation + +class StateService { + static let shared: StateService = StateService() + + let CURRENT_STATE_KEY = "current_state_key" + let CURRENT_USER_KEY = "current_user_key" +// let TIMEOUT_MINUTES_KEY = "timeout_minutes_key" +// let TIMEOUT_ACTION_KEY = "timeout_action_key" + + private init(){} + + var currentState:BWState { + get { + guard let stateData = KeychainHelper.standard.read(CURRENT_STATE_KEY), + let strData = String(data: stateData, encoding: .utf8), + let intData = Int(strData), + let state = BWState(rawValue: intData) else { + return BWState.needSetup + } + + return state + } + set(newState) { + let stateVal = String(newState.rawValue) + KeychainHelper.standard.save(stateVal.data(using: .utf8)!, CURRENT_STATE_KEY) + } + } + + func getUser() -> User? { + return KeychainHelper.standard.read(CURRENT_USER_KEY, User.self) + } + + func setUser(user: User?) { + guard let user = user else { + KeychainHelper.standard.delete(CURRENT_USER_KEY) + return + } + + KeychainHelper.standard.save(user, key: CURRENT_USER_KEY) + } + +// var vaultTimeoutInMinutes: Int? { +// guard let timeoutData = KeychainHelper.standard.read(TIMEOUT_MINUTES_KEY), +// let strData = String(data: timeoutData, encoding: .utf8), +// let intVal = Int(strData) else { +// return nil +// } +// +// return intVal +// } + +// var vaultTimeoutAction: VaultTimeoutAction { +// guard let timeoutActionData = KeychainHelper.standard.read(TIMEOUT_ACTION_KEY), +// let strData = String(data: timeoutActionData, encoding: .utf8), +// let intData = Int(strData), +// let timeoutAction = VaultTimeoutAction(rawValue: intData) else { +// return .lock +// } +// +// return timeoutAction +// } + +// func setVaultTimeout(_ timeoutInMinutes: Int?, _ action: VaultTimeoutAction) { +// guard let timeoutInMinutes = timeoutInMinutes else { +// KeychainHelper.standard.delete(TIMEOUT_MINUTES_KEY) +// return +// } +// +// KeychainHelper.standard.save(String(timeoutInMinutes).data(using: .utf8)!, TIMEOUT_MINUTES_KEY) +// KeychainHelper.standard.save(String(action.rawValue).data(using: .utf8)!, TIMEOUT_ACTION_KEY) +// } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/TotpService.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/TotpService.swift new file mode 100644 index 000000000..f851bc365 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/TotpService.swift @@ -0,0 +1,133 @@ +import Foundation +import CryptoKit + +class TotpService{ + struct CodeConfig{ + let period:Int + let digits:Int + let algorithm:CryptoFunctionService.CryptoHashAlgorithm + let keyB32:String? + } + + static let shared: TotpService = TotpService() + + static let STEAM_CHARS = "23456789BCDFGHJKMNPQRTVWXY"; + static let TOTP_DEFAULT_TIMER: Int = 30 + + func GetCodeAsync(key: String?) throws -> String?{ + guard let key = key, !key.isEmpty else { + return nil + } + + var config = CodeConfig(period: TotpService.TOTP_DEFAULT_TIMER, digits: 6, algorithm: CryptoFunctionService.CryptoHashAlgorithm.Sha1, keyB32: key) + + let isOtpAuth = key.lowercased().starts(with: "otpauth://"); + let isSteamAuth = key.lowercased().starts(with: "steam://"); + + if isOtpAuth { + guard let keyUrl = URLComponents(string: key) else { + return nil + } + + if let queryItems = keyUrl.queryItems { + config = getCodeConfigFrom(queryItems, config) + } + } else if isSteamAuth { + let keyIndexOffset = key.index(key.startIndex, offsetBy: 8) + + config = CodeConfig(period: config.period, digits: 5, algorithm: config.algorithm, keyB32: String(key.suffix(from: keyIndexOffset))) + } + + guard let keyB32 = config.keyB32 else { + return nil + } + + let keyBytes = try Base32.fromBase32(keyB32) + if keyBytes.count == 0 { + return nil + } + + let counter = UInt64(Date().timeIntervalSince1970 / TimeInterval(config.period)).bigEndian + let hash = CryptoFunctionService.shared.hmac(counter.data, SymmetricKey(data:Data(keyBytes)), algorithm: config.algorithm) + if hash.count == 0 + { + return nil; + } + + let offset = Int(hash[hash.count - 1] & 0xf) + let binary = Int32(hash[offset] & 0x7f) << 24 | Int32(hash[offset + 1] & 0xff) << 16 | Int32(hash[offset + 2] & 0xff) << 8 | Int32(hash[offset + 3] & 0xff) + + var otp = ""; + if (isSteamAuth) { + var fullCode = Int(binary & 0x7fffffff) + for _ in 0.. CodeConfig { + var period:Int? + var digits:Int? + var algorithm:CryptoFunctionService.CryptoHashAlgorithm? + var keyB32:String? + + for item in queryItems { + if item.name == "digits", + let digitsVal = item.value, + let digitParam = Int(digitsVal.trimmingCharacters(in: .whitespacesAndNewlines)) { + if (digitParam > 10){ + digits = 10; + } else if (digitParam > 0){ + digits = digitParam; + } + } else if item.name == "period", + let periodVal = item.value, + let periodParam = Int(periodVal.trimmingCharacters(in: .whitespacesAndNewlines)), + periodParam > 0 { + period = periodParam + } else if item.name == "secret", let secretVal = item.value { + keyB32 = secretVal + } else if item.name == "algorithm", let algorithmVal = item.value { + if algorithmVal.lowercased() == "sha256" { + algorithm = CryptoFunctionService.CryptoHashAlgorithm.Sha256; + } + else if algorithmVal.lowercased() == "sha512" { + algorithm = CryptoFunctionService.CryptoHashAlgorithm.Sha512; + } + } + } + + return CodeConfig(period: period ?? currentConfig.period, + digits: digits ?? currentConfig.digits, + algorithm: algorithm ?? currentConfig.algorithm, + keyB32: keyB32 ?? currentConfig.keyB32) + } + + func getPeriodFrom(_ key: String) -> Int { + guard key.lowercased().starts(with: "otpauth://"), + let keyUrl = URLComponents(string: key), + let queryItems = keyUrl.queryItems else { + return TotpService.TOTP_DEFAULT_TIMER + } + + let periodQueryItem = queryItems.first { qi in qi.name == "period" } + guard let periodValue = periodQueryItem?.value, + let periodInt = Int(periodValue.trimmingCharacters(in: .whitespacesAndNewlines)), + periodInt > 0 else { + return TotpService.TOTP_DEFAULT_TIMER + } + return periodInt + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/BWState.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/BWState.swift new file mode 100644 index 000000000..bc4c22ed1 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/BWState.swift @@ -0,0 +1,15 @@ +import Foundation + +enum BWState : Int, Codable { + case valid = 0 + case needLogin = 1 + case needPremium = 2 + case needSetup = 3 + case need2FAItem = 4 + case syncing = 5 + // case needUnlock = 6 + + var isDestructive: Bool { + return self == .needSetup || self == .needLogin || self == .needPremium || self == .need2FAItem + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/Base32.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/Base32.swift new file mode 100644 index 000000000..ca58bb278 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/Base32.swift @@ -0,0 +1,58 @@ +import Foundation + +enum Base32Error: Error { + case invalidFormat +} + +final class Base32 { + private static let BASE_32_CHARS:String = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" + + static func fromBase32(_ rawInput: String) throws -> [UInt8] { + var input = rawInput.uppercased() + var cleanedInput = ""; + for c in input { + if (BASE_32_CHARS.firstIndex(of: c) != nil){ + cleanedInput.append(c); + } + } + + input = cleanedInput; + if input.count == 0 { + return [UInt8]() + } + + var output = [UInt8](repeating: 0, count: input.count * 5 / 8) // new byte[input.Length * 5 / 8]; + var bitIndex = 0; + var inputIndex = 0; + var outputBits = 0; + var outputIndex = 0; + + while outputIndex < output.count { + guard let byteIndex = BASE_32_CHARS.firstIndex(of: input[input.index(input.startIndex, offsetBy:inputIndex)]) else { + throw Base32Error.invalidFormat + } + + let byteIndexInt = BASE_32_CHARS.distance(from: BASE_32_CHARS.startIndex, to: byteIndex) + + let bits = min(5 - bitIndex, 8 - outputBits); + output[outputIndex] <<= bits; + output[outputIndex] |= (UInt8)(byteIndexInt >> (5 - (bitIndex + bits))); + + bitIndex += bits; + if (bitIndex >= 5) + { + inputIndex += 1; + bitIndex = 0; + } + + outputBits += bits; + if (outputBits >= 8) + { + outputIndex += 1; + outputBits = 0; + } + } + + return output; + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ColorUtils.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ColorUtils.swift new file mode 100644 index 000000000..58242e640 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ColorUtils.swift @@ -0,0 +1,60 @@ +import SwiftUI + +extension Color { + static let ui = Color.UI() + + struct UI { + let primary = Color(hex: "#175DDC") + let itemBackground = Color("ItemBackground") + let darkTextMuted = Color("DarkTextMuted") + } + + init?(hex: String) { + var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) + hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") + + var rgb: UInt64 = 0 + + var r: CGFloat = 0.0 + var g: CGFloat = 0.0 + var b: CGFloat = 0.0 + var a: CGFloat = 1.0 + + guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else{ + return nil + } + + switch hexSanitized.count { + case 3: + r = CGFloat((rgb >> 8) * 17) / 255.0 + g = CGFloat((rgb >> 4 & 0xF) * 17) / 255.0 + b = CGFloat((rgb & 0xF) * 17) / 255.0 + case 6: + r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0 + g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0 + b = CGFloat(rgb & 0x0000FF) / 255.0 + case 8: + r = CGFloat((rgb & 0xFF000000) >> 24) / 255.0 + g = CGFloat((rgb & 0x00FF0000) >> 16) / 255.0 + b = CGFloat((rgb & 0x0000FF00) >> 8) / 255.0 + a = CGFloat(rgb & 0x000000FF) / 255.0 + default: + return nil + } + + self.init(red: r, green: g, blue: b, opacity: a) + } + + var components: (red: CGFloat, green: CGFloat, blue: CGFloat, opacity: CGFloat) { + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var o: CGFloat = 0 + + guard UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &o) else { + return (0, 0, 0, 0) + } + + return (r, g, b, o) + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/DateExtensions.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/DateExtensions.swift new file mode 100644 index 000000000..8ba7d97e6 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/DateExtensions.swift @@ -0,0 +1,8 @@ +import Foundation + +extension Date{ + var epocUtcNowInMs: Int { + return Int(self.timeIntervalSince1970 * 1_000) + } + +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/EmptyStateViewModifier.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/EmptyStateViewModifier.swift new file mode 100644 index 000000000..eaed3c430 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/EmptyStateViewModifier.swift @@ -0,0 +1,23 @@ +import Foundation +import SwiftUI + +struct EmptyStateViewModifier: ViewModifier where EmptyContent: View { + var isEmpty: Bool + let emptyContent: () -> EmptyContent + + func body(content: Content) -> some View { + if isEmpty { + emptyContent() + } + else { + content + } + } +} + +extension View { + func emptyState(_ isEmpty: Bool, + emptyContent: @escaping () -> EmptyContent) -> some View where EmptyContent: View { + modifier(EmptyStateViewModifier(isEmpty: isEmpty, emptyContent: emptyContent)) + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ErrorExtensions.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ErrorExtensions.swift new file mode 100644 index 000000000..6320fe5ca --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ErrorExtensions.swift @@ -0,0 +1,69 @@ +import Foundation + +let theOperationCouldNotBeCompleted = "The operation could not be completed." + +extension Error { + /// - Returns: A fully qualified representation of this error. + public var legibleDescription: String { + switch errorType { + case .swiftError(.enum?), .swiftLocalizedError(_, .enum?): + return "\(type(of: self)).\(self)" + case .swiftError(.class?), .swiftLocalizedError(_, .class?): + return "\(type(of: self))" + case .swiftError, .swiftLocalizedError: + return String(describing: self) + case let .nsError(nsError, domain, code): + if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError { + return "\(domain)(\(code), \(underlyingError.domain)(\(underlyingError.code)))" + } else { + return "\(domain)(\(code))" + } + } + } + + /// - Returns: A fully qualified, user-visible representation of this error. + public var legibleLocalizedDescription: String { + switch errorType { + case .swiftError: + return "\(theOperationCouldNotBeCompleted) (\(legibleDescription))" + case .swiftLocalizedError(let msg, _): + return msg + case .nsError(_, "kCLErrorDomain", 0): + return "The location could not be determined." + // ^^ Apple don’t provide a localized description for this + case let .nsError(nsError, domain, code): + if !localizedDescription.hasPrefix(theOperationCouldNotBeCompleted) { + return localizedDescription + //FIXME ^^ for non-EN + } else if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? Error { + return underlyingError.legibleLocalizedDescription + } else { + // usually better than the localizedDescription, but not pretty + return "\(theOperationCouldNotBeCompleted) (\(domain).\(code))" + } + } + } + + private var errorType: ErrorType { + let foo: Any = self + let nativeClassNames = ["_SwiftNativeNSError", "__SwiftNativeNSError"] + let selfClassName = String(cString: object_getClassName(self)) + let isNSError = !nativeClassNames.contains(selfClassName) && foo is NSObject + // ^^ ∵ otherwise implicit bridging implicitly casts as for other tests + + if isNSError { + let nserr = self as NSError + return .nsError(nserr, domain: nserr.domain, code: nserr.code) + } else if let err = self as? LocalizedError, let msg = err.errorDescription { + return .swiftLocalizedError(msg, Mirror(reflecting: self).displayStyle) + } else { + return .swiftError(Mirror(reflecting: self).displayStyle) + } + } +} + +private enum ErrorType { + case nsError(NSError, domain: String, code: Int) + case swiftLocalizedError(String, Mirror.DisplayStyle?) + case swiftError(Mirror.DisplayStyle?) +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/JsonDecoderExtensions.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/JsonDecoderExtensions.swift new file mode 100644 index 000000000..c40a0303b --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/JsonDecoderExtensions.swift @@ -0,0 +1,40 @@ +import Foundation + +extension JSONDecoder.KeyDecodingStrategy { + static var upperToLowerCamelCase: JSONDecoder.KeyDecodingStrategy { + return .custom { codingKeys in + + var key = AnyCodingKey(codingKeys.last!) + + if let firstChar = key.stringValue.first { + key.stringValue.replaceSubrange( + ...key.stringValue.startIndex, with: String(firstChar).lowercased() + ) + } + return key + } + } +} + +struct AnyCodingKey : CodingKey { + var stringValue: String + var intValue: Int? + + init(_ base: CodingKey) { + self.init(stringValue: base.stringValue, intValue: base.intValue) + } + + init(stringValue: String) { + self.stringValue = stringValue + } + + init(intValue: Int) { + self.stringValue = "\(intValue)" + self.intValue = intValue + } + + init(stringValue: String, intValue: Int?) { + self.stringValue = stringValue + self.intValue = intValue + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/StringExtensions.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/StringExtensions.swift new file mode 100644 index 000000000..b5441d771 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/StringExtensions.swift @@ -0,0 +1,28 @@ +import Foundation + +extension String { + static func isEmpty(_ s:String?) -> Bool { + guard let s = s else { + return true + } + + return s.isEmpty + } + + static func isEmptyOrWhitespace(_ s: String?) -> Bool { + guard let s = s else { + return true + } + + return s.trimmingCharacters(in: .whitespaces).isEmpty + } + + func leftPadding(toLength: Int, withPad character: Character) -> String { + let currentLength = self.count + if currentLength < toLength { + return String(repeatElement(character, count: toLength - currentLength)) + self + } else { + return String(self.suffix(toLength)) + } + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/UInt64Extensions.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/UInt64Extensions.swift new file mode 100644 index 000000000..83f5b7c84 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/UInt64Extensions.swift @@ -0,0 +1,9 @@ +import Foundation + +extension UInt64 { + var data: Data { + var int64 = self + let int64Data = Data(bytes: &int64, count: MemoryLayout.size(ofValue: self)) + return int64Data + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/URLExtensions.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/URLExtensions.swift new file mode 100644 index 000000000..c8221e80a --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/URLExtensions.swift @@ -0,0 +1,28 @@ +import Foundation + +extension URL { + static func createFullUri(from uriString:String?) -> URL? { + guard let uriString = uriString else { + return nil + } + + let hasHttpScheme = uriString.starts(with: "http://") || uriString.starts(with: "https://") + if !hasHttpScheme && !uriString.contains("://") && uriString.contains(".") { + if let uri = URL(string: "http://\(uriString)") { + return uri + } + } + guard let uri2 = URL(string: uriString) else { + return nil + } + + return uri2 + } + + var host:String? { + if let components = URLComponents(url: self, resolvingAgainstBaseURL: false){ + return components.host + } + return nil + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ViewExtensions.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ViewExtensions.swift new file mode 100644 index 000000000..d008fa74d --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ViewExtensions.swift @@ -0,0 +1,15 @@ +import Foundation +import SwiftUI + +extension View { + func placeholder( + when shouldShow: Bool, + alignment: Alignment = .leading, + @ViewBuilder placeholder: () -> Content) -> some View { + + ZStack(alignment: alignment) { + placeholder().opacity(shouldShow ? 1 : 0) + self + } + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/ViewModels/BWStateViewModel.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/ViewModels/BWStateViewModel.swift new file mode 100644 index 000000000..ac841086e --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/ViewModels/BWStateViewModel.swift @@ -0,0 +1,26 @@ +import Foundation + +class BWStateViewModel : ObservableObject{ + @Published var text:String + @Published var isLoading:Bool = false + + init(_ state: BWState){ + switch state { + case .needLogin: + text = "LogInToBitwardenOnYourIPhoneToViewVerificationCodes" +// case .needUnlock: +// text = "UnlockBitwardenOnYourIPhoneToViewVerificationCodes" + case .needPremium: + text = "ToViewVerificationCodesUpgradeToPremium" + case .needSetup: + text = "SetUpBitwardenToViewItemsContainingVerificationCodes" + case .syncing: + text = "SyncingItemsContainingVerificationCodes" + isLoading = true + case .need2FAItem: + text = "Add2FactorAutenticationToAnItemToViewVerificationCodes" + default: + text = "" + } + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/ViewModels/CipherDetailsViewModel.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/ViewModels/CipherDetailsViewModel.swift new file mode 100644 index 000000000..bfed0a30b --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/ViewModels/CipherDetailsViewModel.swift @@ -0,0 +1,63 @@ +import Foundation +import SwiftUI + +class CipherDetailsViewModel: ObservableObject{ + @Published var cipher:Cipher + + @Published var totpFormatted:String = "" + @Published var progress:Double = 1 + @Published var counter:Int + @Published var iconImageUri:String? + + var key: String + var period: Int + var timer: Timer? = nil + + init(cipher: Cipher) { + self.cipher = cipher + self.key = cipher.login.totp! + self.period = TotpService.shared.getPeriodFrom(key) + self.counter = period + self.iconImageUri = IconImageHelper.shared.getLoginIconImage(cipher) + } + + func startGeneration() { + self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] t in + guard let self = self else { + t.invalidate() + return + } + + let epoc = Int64(Date().timeIntervalSince1970) + let mod = Int(epoc % Int64(self.period)) + DispatchQueue.main.async { + self.counter = self.period - mod + self.progress = Double(self.counter) / Double(self.period) + } + if mod == 0 || self.totpFormatted == "" { + do { + var totpF = try TotpService.shared.GetCodeAsync(key: self.key) ?? "" + if totpF.count > 4 { + let halfIndex = totpF.index(totpF.startIndex, offsetBy: totpF.count / 2) + totpF = "\(totpF[totpF.startIndex.. Bool { + if searchTerm.isEmpty { + return true + } + + if(cipher.name?.lowercased().contains(searchTerm.lowercased()) ?? false) { + return true + } + + if (cipher.login.username?.lowercased().contains(searchTerm.lowercased()) ?? false) { + return true + } + + return false + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/BWStateView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/BWStateView.swift new file mode 100644 index 000000000..05436a17c --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/BWStateView.swift @@ -0,0 +1,37 @@ +import SwiftUI + +struct BWStateView: View { + @ObservedObject var viewModel:BWStateViewModel + + init(_ state: BWState) { + viewModel = BWStateViewModel(state) + } + + var body: some View { + VStack(alignment: .center) { + Spacer() + Image("BitwardenImagetype") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: .infinity, height: 35) + .padding(.leading, 15) + .padding(.trailing, 15) + .padding(.top, 5) + Spacer() + Text(LocalizedStringKey(viewModel.text)) + .font(.title3) + .fontWeight(.semibold) + .multilineTextAlignment(.center) + if viewModel.isLoading { + ProgressView() + .frame(width: 20, height: 20) + } + } + } +} + +struct BWStateView_Previews: PreviewProvider { + static var previews: some View { + BWStateView(.needSetup) + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherDetailsView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherDetailsView.swift new file mode 100644 index 000000000..7ac031004 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherDetailsView.swift @@ -0,0 +1,101 @@ +import SwiftUI + +struct CipherDetailsView: View { + @ObservedObject var cipherDetailsViewModel: CipherDetailsViewModel + + let iconSize: CGSize = CGSize(width: 30, height: 30) + + init(cipher: Cipher) { + self.cipherDetailsViewModel = CipherDetailsViewModel(cipher: cipher) + } + + var body: some View { + VStack(alignment:.leading){ + HStack{ + if cipherDetailsViewModel.iconImageUri == nil { + iconPlaceholderImage + } else { + if #available(watchOSApplicationExtension 8.0, *) { + AsyncImage(url: URL(string: cipherDetailsViewModel.iconImageUri!)){ phase in + switch phase { + case .empty: + iconPlaceholderImage + case .success(let image): + image.resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: iconSize.width, maxHeight: iconSize.height) + case .failure: + iconPlaceholderImage + @unknown default: + EmptyView() + } + } + } else { + ImageView(withURL: cipherDetailsViewModel.iconImageUri!, maxWidth: iconSize.width, maxHeight: iconSize.height) { + iconPlaceholderImage + } + } + } + Text(cipherDetailsViewModel.cipher.name!) + .font(.title2) + .fontWeight(.bold) + .lineLimit(1) + .truncationMode(.tail) + .padding(.leading, 5) + } + if cipherDetailsViewModel.cipher.login.username != nil { + Text(cipherDetailsViewModel.cipher.login.username!) + .font(.title3) + .fontWeight(.bold) + .lineLimit(1) + .truncationMode(.tail) + } + if cipherDetailsViewModel.totpFormatted == "" { + ProgressView() + } else { + HStack{ + let transition = AnyTransition.asymmetric(insertion: .slide, removal: .scale).combined(with: .opacity) + Text(cipherDetailsViewModel.totpFormatted) + .font(.largeTitle) + .scaledToFit() + .minimumScaleFactor(0.01) + .lineLimit(1) + .id(cipherDetailsViewModel.totpFormatted) + .transition(transition) + .animation(.default.speed(0.7), value: cipherDetailsViewModel.totpFormatted) + Spacer() + ZStack{ + CircularProgressView(progress: cipherDetailsViewModel.progress, strokeLineWidth: 3, strokeColor: Color.blue, endingStrokeColor: Color.red) + .frame(width: 40, height:40) + Text("\(cipherDetailsViewModel.counter)") + .font(.title3) + .fontWeight(.semibold) + } + } + .padding(.top, 20) + .padding(.leading, 5) + .padding(.trailing, 5) + } + } + .onAppear{ + self.cipherDetailsViewModel.startGeneration() + } + .onDisappear{ + self.cipherDetailsViewModel.stopGeneration() + } + } + + var iconPlaceholderImage: some View{ + Image("DefaultCipherIcon") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: iconSize.width, maxHeight: iconSize.height) + + } +} + +struct CipherDetailsView_Previews: PreviewProvider { + static var previews: some View { + CipherDetailsView(cipher: CipherMock.ciphers[0]) + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherItemView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherItemView.swift new file mode 100644 index 000000000..4747a064c --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherItemView.swift @@ -0,0 +1,57 @@ +import SwiftUI + +struct CipherItemView: View { + let cipher:Cipher + let maxWidth:CGFloat + + init(_ cipher:Cipher, _ maxWidth:CGFloat) { + self.cipher = cipher + self.maxWidth = maxWidth + } + + var body: some View { + VStack(alignment: .leading) { + if cipher.id == "-1" { + // Workaround: To display 0 results on search + // and the message to be localized + Text(LocalizedStringKey(cipher.name!)) + .font(.title3) + .fontWeight(.bold) + .lineLimit(1) + .truncationMode(.tail) + .frame(maxWidth: .infinity, alignment: .leading) + } else { + Text(cipher.name ?? "") + .font(.title3) + .fontWeight(.bold) + .lineLimit(1) + .truncationMode(.tail) + .frame(maxWidth: .infinity, alignment: .leading) + } + + if cipher.login.username != nil { + Text(cipher.login.username! ) + .font(.body) + .lineLimit(1) + .truncationMode(.tail) + .foregroundColor(Color.ui.darkTextMuted) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .padding() + .background( + RoundedRectangle(cornerRadius: 5) + .foregroundColor(Color.ui.itemBackground) + .frame(width: maxWidth, + alignment: .leading) + ) + .frame(width: maxWidth, + alignment: .leading) + } +} + +struct CipherItemView_Previews: PreviewProvider { + static var previews: some View { + CipherItemView(CipherMock.ciphers[0], .infinity) + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherListView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherListView.swift new file mode 100644 index 000000000..4c40568a0 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherListView.swift @@ -0,0 +1,134 @@ +import SwiftUI + +struct CipherListView: View { + @ObservedObject var viewModel = CipherListViewModel(CipherService.shared) + + let AVATAR_ID: String = "avatarId" + @State private var contentOffset = CGFloat(0) + @State private var initialOffset = CGFloat(0) + + var isHeaderVisible: Bool { + if !viewModel.searchTerm.isEmpty { + return true + } + + let threshold = initialOffset + 15 + return viewModel.filteredCiphers.count > 1 && contentOffset > threshold + } + + var body: some View { + NavigationView { + GeometryReader { geometry in + ScrollViewReader { scrollProxy in + TrackableWithHeaderListView { offset in + withAnimation { + contentOffset = offset?.y ?? 0 + } + } headerContent: { + Section() { + ZStack { + searchContent + .padding(5) + .background( + RoundedRectangle(cornerRadius: 5) + .foregroundColor(Color.ui.primary) + .frame(width: geometry.size.width, + alignment: .leading) + ) + .opacity(isHeaderVisible ? 1 : 0) + } + .background( + RoundedRectangle(cornerRadius: 5) + .foregroundColor(Color.black) + .frame(width: geometry.size.width, height: 60) + ) + .offset(y:isHeaderVisible ? 0 : 5) + .padding(0) + } + } content: { + if viewModel.user?.email != nil { + Section() { + avatarHeader + .id(AVATAR_ID) + } + } + ForEach(viewModel.filteredCiphers, id: \.id) { cipher in + NavigationLink(destination: CipherDetailsView(cipher: cipher)){ + CipherItemView(cipher, geometry.size.width) + } + .listRowInsets(EdgeInsets()) + .listRowBackground(Color.clear) + .padding(3) + } + } + .emptyState(viewModel.filteredCiphers.isEmpty, emptyContent: { + emptyContent + .frame(width: geometry.size.width, alignment: .center) + }) + .onReceive(self.viewModel.$updateHack) { _ in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + scrollProxy.scrollTo(AVATAR_ID, anchor: .top) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15){ + self.initialOffset = self.contentOffset + } + } + } + } + } + } + .onAppear { + self.viewModel.checkStateAndFetch() + } + .fullScreenCover(isPresented: $viewModel.showingSheet) { + BWStateView(viewModel.currentState) + } + } + + var searchContent: some View { + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(Color(.white)) + .frame(width: 20, height: 30) + TextField("", text: $viewModel.searchTerm) + .foregroundColor(.white) + .frame(width: .infinity, height: 33) + .placeholder(when: viewModel.searchTerm.isEmpty) { + Text("Search").foregroundColor(.white) + } + } + } + + var avatarHeader: some View { + HStack { + AvatarView(viewModel.user) + Text(viewModel.user!.email!) + .font(.headline) + .lineLimit(1) + .truncationMode(.tail) + } + } + + var emptyContent: some View { + VStack(alignment: .center) { + Image("EmptyListPlaceholder") + .resizable() + .scaledToFit() + .padding(20) + Text("ThereAreNoItemsToList") + .foregroundColor(Color.white) + .font(.headline) + .multilineTextAlignment(.center) + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + var v = CipherListView() + StateService.shared.currentState = .valid + v.viewModel = CipherListViewModel(CipherServiceMock()) + v.viewModel.user = User(id: "zxc", email: "testing@test.com", name: "Tester") + return v + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/WatchConnectivityManager.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/WatchConnectivityManager.swift new file mode 100644 index 000000000..03908d36f --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/WatchConnectivityManager.swift @@ -0,0 +1,116 @@ +import Combine +import Foundation +import WatchConnectivity + +struct WatchConnectivityMessage { + var state: BWState? +} + +final class WatchConnectivityManager: NSObject, ObservableObject { + static let shared = WatchConnectivityManager() + + let watchConnectivitySubject = CurrentValueSubject(WatchConnectivityMessage(state: nil)) + + private let kMessageKey = "message" + private let kCipherDataKey = "watchDto" + + private override init() { + super.init() + + if WCSession.isSupported() { + WCSession.default.delegate = self + WCSession.default.activate() + } + } + + var isSessionActivated: Bool { + return WCSession.default.isCompanionAppInstalled && WCSession.default.activationState == .activated + } + + func send(_ message: String) { + guard WCSession.default.activationState == .activated else { + return + } + + guard WCSession.default.isCompanionAppInstalled else { + return + } + + WCSession.default.sendMessage([kMessageKey : message], replyHandler: nil) { error in + Log.e("Cannot send message: \(String(describing: error))") + } + } +} + +extension WatchConnectivityManager: WCSessionDelegate { + func session(_ session: WCSession, didReceiveMessage message: [String : Any]) { + } + + func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { + } + + func session(_ session: WCSession, + activationDidCompleteWith activationState: WCSessionActivationState, + error: Error?) { + } + + func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) { + // in order for the delivery to be faster the time is added to the key to make each application context update have a different key + // and update faster + let watchDtoKey = applicationContext.keys.first { k in + k.starts(with: kCipherDataKey) + } + + guard let dtoKey = watchDtoKey, let serializedDto = applicationContext[dtoKey] as? String else { + return + } + + do { + guard let json = try! JSONSerialization.jsonObject(with: serializedDto.data(using: .utf8)!, options: [.fragmentsAllowed]) as? String else { + return + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .upperToLowerCamelCase + let watchDTO = try decoder.decode(WatchDTO.self, from: json.data(using: .utf8)!) + + let previousUserId = StateService.shared.getUser()?.id + + if previousUserId != watchDTO.userData?.id { + self.watchConnectivitySubject.send(WatchConnectivityMessage(state: .syncing)) + } + + StateService.shared.currentState = watchDTO.state + StateService.shared.setUser(user: watchDTO.userData) +// StateService.shared.setVaultTimeout(watchDTO.settingsData?.vaultTimeoutInMinutes, watchDTO.settingsData?.vaultTimeoutAction ?? .lock) + EnvironmentService.shared.baseUrl = watchDTO.environmentData?.base + EnvironmentService.shared.setIconsUrl(url: watchDTO.environmentData?.icons) + + if watchDTO.state.isDestructive { + CipherService.shared.deleteAll(nil) { + self.watchConnectivitySubject.send(WatchConnectivityMessage(state: nil)) + } + } + + if watchDTO.state == .valid, var ciphers = watchDTO.ciphers { + // we need to track the to which user the ciphers belong to, so we add the user here to all ciphers + // note: it's not being sent directly from the phone to increase performance on the communication + ciphers.indices.forEach { i in + ciphers[i].userId = watchDTO.userData!.id + } + + CipherService.shared.saveCiphers(ciphers) { + if let previousUserId = previousUserId, + let currentUserid = watchDTO.userData?.id, + previousUserId != currentUserid { + CipherService.shared.deleteAll(previousUserId) {} + } + self.watchConnectivitySubject.send(WatchConnectivityMessage(state: nil)) + } + } + } + catch { + Log.e(error) + } + } +} diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/bitwardenApp.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/bitwardenApp.swift new file mode 100644 index 000000000..a10645018 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/bitwardenApp.swift @@ -0,0 +1,14 @@ +import SwiftUI + +@main +struct bitwardenApp: App { + @SceneBuilder var body: some Scene { + WindowGroup { + NavigationView { + CipherListView() + } + } + + WKNotificationScene(controller: NotificationController.self, category: "myCategory") + } +} diff --git a/src/watchOS/bitwarden/bitwarden.xcodeproj/project.pbxproj b/src/watchOS/bitwarden/bitwarden.xcodeproj/project.pbxproj new file mode 100644 index 000000000..1a54dc894 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden.xcodeproj/project.pbxproj @@ -0,0 +1,987 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 1B0A1A9E28ECD7C400FF61CD /* WatchConnectivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B0A1A9D28ECD7C400FF61CD /* WatchConnectivityManager.swift */; }; + 1B11C899291BFAB500CE58D8 /* CryptoFunctionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B11C898291BFAB500CE58D8 /* CryptoFunctionService.swift */; }; + 1B11C89B291C587600CE58D8 /* UInt64Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B11C89A291C587600CE58D8 /* UInt64Extensions.swift */; }; + 1B14DF37291186D900EA43F1 /* EmptyStateViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B14DF36291186D900EA43F1 /* EmptyStateViewModifier.swift */; }; + 1B15613328B7F3D400610B9B /* bitwardenApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15613228B7F3D400610B9B /* bitwardenApp.swift */; }; + 1B15613528B7F3D400610B9B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15613428B7F3D400610B9B /* ContentView.swift */; }; + 1B15613728B7F3D700610B9B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1B15613628B7F3D700610B9B /* Assets.xcassets */; }; + 1B15613A28B7F3D700610B9B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1B15613928B7F3D700610B9B /* Preview Assets.xcassets */; }; + 1B15613E28B7F3D700610B9B /* Bitwarden.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 1B15613D28B7F3D700610B9B /* Bitwarden.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 1B15614328B7F3D800610B9B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1B15614228B7F3D800610B9B /* Assets.xcassets */; }; + 1B15614928B7F3D800610B9B /* bitwarden WatchKit Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 1B15614828B7F3D800610B9B /* bitwarden WatchKit Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 1B15614E28B7F3D800610B9B /* bitwardenApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15614D28B7F3D800610B9B /* bitwardenApp.swift */; }; + 1B15615028B7F3D800610B9B /* CipherListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15614F28B7F3D800610B9B /* CipherListView.swift */; }; + 1B15615228B7F3D800610B9B /* NotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15615128B7F3D800610B9B /* NotificationController.swift */; }; + 1B15615428B7F3D800610B9B /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15615328B7F3D800610B9B /* NotificationView.swift */; }; + 1B15615628B7F3D800610B9B /* ComplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15615528B7F3D800610B9B /* ComplicationController.swift */; }; + 1B15615828B7F3D900610B9B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1B15615728B7F3D900610B9B /* Assets.xcassets */; }; + 1B15615B28B7F3D900610B9B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1B15615A28B7F3D900610B9B /* Preview Assets.xcassets */; }; + 1B15616C28B81A2200610B9B /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15616B28B81A2200610B9B /* Cipher.swift */; }; + 1B15616E28B81A4300610B9B /* WatchConnectivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15616D28B81A4300610B9B /* WatchConnectivityManager.swift */; }; + 1B59EC5729007DEE00A8718D /* BitwardenDB.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1B59EC5529007DEE00A8718D /* BitwardenDB.xcdatamodeld */; }; + 1B59EC592900801500A8718D /* StringEncryptionTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B59EC582900801500A8718D /* StringEncryptionTransformer.swift */; }; + 1B59EC5C2900BB3400A8718D /* CryptoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B59EC5B2900BB3400A8718D /* CryptoService.swift */; }; + 1B59EC612900C48E00A8718D /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B59EC602900C48E00A8718D /* KeychainHelper.swift */; }; + 1B59EC632901B1C100A8718D /* LoggerHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B59EC622901B1C100A8718D /* LoggerHelper.swift */; }; + 1B5AFF0329196C81004478F9 /* ColorUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B5AFF0229196C81004478F9 /* ColorUtils.swift */; }; + 1B5AFF0729197822004478F9 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1B5AFF0929197822004478F9 /* Localizable.strings */; }; + 1B5F5E38293F9CF8009B5FCC /* TrackableWithHeaderListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B5F5E37293F9CF8009B5FCC /* TrackableWithHeaderListView.swift */; }; + 1B5F5E3A293F9D6F009B5FCC /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B5F5E39293F9D6F009B5FCC /* ViewExtensions.swift */; }; + 1B5F5E3E293FBB17009B5FCC /* CipherItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B5F5E3D293FBB17009B5FCC /* CipherItemView.swift */; }; + 1B6BD10229364F020041982D /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6BD10129364F020041982D /* AvatarView.swift */; }; + 1B8453EC290C672E00F921E1 /* CipherEntity+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8453EA290C672E00F921E1 /* CipherEntity+CoreDataClass.swift */; }; + 1B8453ED290C672E00F921E1 /* CipherEntity+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8453EB290C672E00F921E1 /* CipherEntity+CoreDataProperties.swift */; }; + 1B8BF90429199BBB006F069E /* CipherDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8BF90329199BBB006F069E /* CipherDetailsView.swift */; }; + 1B8BF90629199EC5006F069E /* CipherDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8BF90529199EC5006F069E /* CipherDetailsViewModel.swift */; }; + 1B8BF9092919A2CC006F069E /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8BF9082919A2CC006F069E /* CircularProgressView.swift */; }; + 1B8BF90B2919AF2A006F069E /* TotpService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8BF90A2919AF2A006F069E /* TotpService.swift */; }; + 1B8BF90D2919BED9006F069E /* Base32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8BF90C2919BED9006F069E /* Base32.swift */; }; + 1B8BF9112919CDBB006F069E /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8BF9102919CDBB006F069E /* DateExtensions.swift */; }; + 1BC1CD6329227D3C006540DA /* EnvironmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC1CD6229227D3C006540DA /* EnvironmentService.swift */; }; + 1BC1CD6529227F3C006540DA /* IconImageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC1CD6429227F3C006540DA /* IconImageHelper.swift */; }; + 1BC1CD672922871A006540DA /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC1CD662922871A006540DA /* URLExtensions.swift */; }; + 1BC1CD6929228CEB006540DA /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC1CD6829228CEB006540DA /* StringExtensions.swift */; }; + 1BC1CD6C29229D1B006540DA /* CipherMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC1CD6B29229D1B006540DA /* CipherMock.swift */; }; + 1BC1CD6E2922B92B006540DA /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC1CD6D2922B92B006540DA /* ImageView.swift */; }; + 1BD291B32924043C0004F33F /* BWStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291B22924043C0004F33F /* BWStateView.swift */; }; + 1BD291B52924047C0004F33F /* BWStateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291B42924047C0004F33F /* BWStateViewModel.swift */; }; + 1BD291B7292409410004F33F /* BWState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291B6292409410004F33F /* BWState.swift */; }; + 1BD291B9292438830004F33F /* StateService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291B8292438830004F33F /* StateService.swift */; }; + 1BD291BB2927E9B50004F33F /* WatchDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291BA2927E9B50004F33F /* WatchDTO.swift */; }; + 1BD291BD292807240004F33F /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291BC292807240004F33F /* User.swift */; }; + 1BD291BF292D0E6F0004F33F /* JsonDecoderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291BE292D0E6F0004F33F /* JsonDecoderExtensions.swift */; }; + 1BD291C1292E7E690004F33F /* ErrorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291C0292E7E690004F33F /* ErrorExtensions.swift */; }; + 1BD291C329311E1C0004F33F /* VaultTimeoutAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291C229311E1C0004F33F /* VaultTimeoutAction.swift */; }; + 1BDBFEAC290B4215009C78C7 /* CipherListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BDBFEAB290B4215009C78C7 /* CipherListViewModel.swift */; }; + 1BDBFEB1290B5BD3009C78C7 /* DBHelperProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BDBFEB0290B5BD3009C78C7 /* DBHelperProtocol.swift */; }; + 1BDBFEB3290B5D07009C78C7 /* CoreDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BDBFEB2290B5D07009C78C7 /* CoreDataHelper.swift */; }; + 1BF5F6DB29103066002DDC0C /* CipherService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF5F6DA29103066002DDC0C /* CipherService.swift */; }; + 1BF5F6DE29103B86002DDC0C /* CipherServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF5F6DD29103B86002DDC0C /* CipherServiceMock.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 1B15613F28B7F3D700610B9B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1B15612728B7F3D400610B9B /* Project object */; + proxyType = 1; + remoteGlobalIDString = 1B15613C28B7F3D700610B9B; + remoteInfo = "bitwarden WatchKit App"; + }; + 1B15614A28B7F3D800610B9B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1B15612728B7F3D400610B9B /* Project object */; + proxyType = 1; + remoteGlobalIDString = 1B15614728B7F3D800610B9B; + remoteInfo = "bitwarden WatchKit Extension"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 1B15616328B7F3D900610B9B /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 1B15614928B7F3D800610B9B /* bitwarden WatchKit Extension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + 1B15616728B7F3D900610B9B /* Embed Watch Content */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; + dstSubfolderSpec = 16; + files = ( + 1B15613E28B7F3D700610B9B /* Bitwarden.app in Embed Watch Content */, + ); + name = "Embed Watch Content"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1B0A1A9D28ECD7C400FF61CD /* WatchConnectivityManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchConnectivityManager.swift; sourceTree = ""; }; + 1B11C898291BFAB500CE58D8 /* CryptoFunctionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFunctionService.swift; sourceTree = ""; }; + 1B11C89A291C587600CE58D8 /* UInt64Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UInt64Extensions.swift; sourceTree = ""; }; + 1B14DF36291186D900EA43F1 /* EmptyStateViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStateViewModifier.swift; sourceTree = ""; }; + 1B15612F28B7F3D400610B9B /* bitwarden.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitwarden.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 1B15613228B7F3D400610B9B /* bitwardenApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = bitwardenApp.swift; sourceTree = ""; }; + 1B15613428B7F3D400610B9B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 1B15613628B7F3D700610B9B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1B15613928B7F3D700610B9B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 1B15613D28B7F3D700610B9B /* Bitwarden.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Bitwarden.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 1B15614228B7F3D800610B9B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1B15614828B7F3D800610B9B /* bitwarden WatchKit Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "bitwarden WatchKit Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 1B15614D28B7F3D800610B9B /* bitwardenApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = bitwardenApp.swift; sourceTree = ""; }; + 1B15614F28B7F3D800610B9B /* CipherListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherListView.swift; sourceTree = ""; }; + 1B15615128B7F3D800610B9B /* NotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationController.swift; sourceTree = ""; }; + 1B15615328B7F3D800610B9B /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = ""; }; + 1B15615528B7F3D800610B9B /* ComplicationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationController.swift; sourceTree = ""; }; + 1B15615728B7F3D900610B9B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1B15615A28B7F3D900610B9B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 1B15615C28B7F3D900610B9B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1B15615D28B7F3D900610B9B /* PushNotificationPayload.apns */ = {isa = PBXFileReference; lastKnownFileType = text; path = PushNotificationPayload.apns; sourceTree = ""; }; + 1B15616B28B81A2200610B9B /* Cipher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cipher.swift; sourceTree = ""; }; + 1B15616D28B81A4300610B9B /* WatchConnectivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConnectivityManager.swift; sourceTree = ""; }; + 1B59EC5629007DEE00A8718D /* BitwardenDB.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = BitwardenDB.xcdatamodel; sourceTree = ""; }; + 1B59EC582900801500A8718D /* StringEncryptionTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringEncryptionTransformer.swift; sourceTree = ""; }; + 1B59EC5B2900BB3400A8718D /* CryptoService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoService.swift; sourceTree = ""; }; + 1B59EC602900C48E00A8718D /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = ""; }; + 1B59EC622901B1C100A8718D /* LoggerHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerHelper.swift; sourceTree = ""; }; + 1B5AFF0229196C81004478F9 /* ColorUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorUtils.swift; sourceTree = ""; }; + 1B5AFF0829197822004478F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 1B5F5E37293F9CF8009B5FCC /* TrackableWithHeaderListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackableWithHeaderListView.swift; sourceTree = ""; }; + 1B5F5E39293F9D6F009B5FCC /* ViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtensions.swift; sourceTree = ""; }; + 1B5F5E3D293FBB17009B5FCC /* CipherItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherItemView.swift; sourceTree = ""; }; + 1B6BD10129364F020041982D /* AvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarView.swift; sourceTree = ""; }; + 1B8453EA290C672E00F921E1 /* CipherEntity+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CipherEntity+CoreDataClass.swift"; sourceTree = ""; }; + 1B8453EB290C672E00F921E1 /* CipherEntity+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CipherEntity+CoreDataProperties.swift"; sourceTree = ""; }; + 1B8BF90329199BBB006F069E /* CipherDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherDetailsView.swift; sourceTree = ""; }; + 1B8BF90529199EC5006F069E /* CipherDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherDetailsViewModel.swift; sourceTree = ""; }; + 1B8BF9082919A2CC006F069E /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = ""; }; + 1B8BF90A2919AF2A006F069E /* TotpService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotpService.swift; sourceTree = ""; }; + 1B8BF90C2919BED9006F069E /* Base32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base32.swift; sourceTree = ""; }; + 1B8BF9102919CDBB006F069E /* DateExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = ""; }; + 1BC1CD6229227D3C006540DA /* EnvironmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentService.swift; sourceTree = ""; }; + 1BC1CD6429227F3C006540DA /* IconImageHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImageHelper.swift; sourceTree = ""; }; + 1BC1CD662922871A006540DA /* URLExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtensions.swift; sourceTree = ""; }; + 1BC1CD6829228CEB006540DA /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; + 1BC1CD6B29229D1B006540DA /* CipherMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherMock.swift; sourceTree = ""; }; + 1BC1CD6D2922B92B006540DA /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; }; + 1BD291B22924043C0004F33F /* BWStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWStateView.swift; sourceTree = ""; }; + 1BD291B42924047C0004F33F /* BWStateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWStateViewModel.swift; sourceTree = ""; }; + 1BD291B6292409410004F33F /* BWState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWState.swift; sourceTree = ""; }; + 1BD291B8292438830004F33F /* StateService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateService.swift; sourceTree = ""; }; + 1BD291BA2927E9B50004F33F /* WatchDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchDTO.swift; sourceTree = ""; }; + 1BD291BC292807240004F33F /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + 1BD291BE292D0E6F0004F33F /* JsonDecoderExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonDecoderExtensions.swift; sourceTree = ""; }; + 1BD291C0292E7E690004F33F /* ErrorExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorExtensions.swift; sourceTree = ""; }; + 1BD291C229311E1C0004F33F /* VaultTimeoutAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultTimeoutAction.swift; sourceTree = ""; }; + 1BDBFEAB290B4215009C78C7 /* CipherListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherListViewModel.swift; sourceTree = ""; }; + 1BDBFEB0290B5BD3009C78C7 /* DBHelperProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBHelperProtocol.swift; sourceTree = ""; }; + 1BDBFEB2290B5D07009C78C7 /* CoreDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHelper.swift; sourceTree = ""; }; + 1BF5F6DA29103066002DDC0C /* CipherService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherService.swift; sourceTree = ""; }; + 1BF5F6DD29103B86002DDC0C /* CipherServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherServiceMock.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1B15612C28B7F3D400610B9B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1B15614528B7F3D800610B9B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1B0A1A9C28E77F4500FF61CD /* Models */ = { + isa = PBXGroup; + children = ( + 1BC1CD6A29229D12006540DA /* Mocks */, + 1B15616B28B81A2200610B9B /* Cipher.swift */, + 1BD291BA2927E9B50004F33F /* WatchDTO.swift */, + 1BD291BC292807240004F33F /* User.swift */, + 1BD291C229311E1C0004F33F /* VaultTimeoutAction.swift */, + ); + path = Models; + sourceTree = ""; + }; + 1B14DF35291186C300EA43F1 /* Utilities */ = { + isa = PBXGroup; + children = ( + 1B14DF36291186D900EA43F1 /* EmptyStateViewModifier.swift */, + 1B5AFF0229196C81004478F9 /* ColorUtils.swift */, + 1B8BF90C2919BED9006F069E /* Base32.swift */, + 1B8BF9102919CDBB006F069E /* DateExtensions.swift */, + 1B11C89A291C587600CE58D8 /* UInt64Extensions.swift */, + 1BC1CD662922871A006540DA /* URLExtensions.swift */, + 1BC1CD6829228CEB006540DA /* StringExtensions.swift */, + 1BD291B6292409410004F33F /* BWState.swift */, + 1BD291BE292D0E6F0004F33F /* JsonDecoderExtensions.swift */, + 1BD291C0292E7E690004F33F /* ErrorExtensions.swift */, + 1B5F5E39293F9D6F009B5FCC /* ViewExtensions.swift */, + ); + path = Utilities; + sourceTree = ""; + }; + 1B15612628B7F3D400610B9B = { + isa = PBXGroup; + children = ( + 1B15613128B7F3D400610B9B /* bitwarden */, + 1B15614128B7F3D700610B9B /* bitwarden WatchKit App */, + 1B15614C28B7F3D800610B9B /* bitwarden WatchKit Extension */, + 1B15613028B7F3D400610B9B /* Products */, + ); + sourceTree = ""; + }; + 1B15613028B7F3D400610B9B /* Products */ = { + isa = PBXGroup; + children = ( + 1B15612F28B7F3D400610B9B /* bitwarden.app */, + 1B15613D28B7F3D700610B9B /* Bitwarden.app */, + 1B15614828B7F3D800610B9B /* bitwarden WatchKit Extension.appex */, + ); + name = Products; + sourceTree = ""; + }; + 1B15613128B7F3D400610B9B /* bitwarden */ = { + isa = PBXGroup; + children = ( + 1B0A1A9D28ECD7C400FF61CD /* WatchConnectivityManager.swift */, + 1B15613228B7F3D400610B9B /* bitwardenApp.swift */, + 1B15613428B7F3D400610B9B /* ContentView.swift */, + 1B15613628B7F3D700610B9B /* Assets.xcassets */, + 1B15613828B7F3D700610B9B /* Preview Content */, + ); + path = bitwarden; + sourceTree = ""; + }; + 1B15613828B7F3D700610B9B /* Preview Content */ = { + isa = PBXGroup; + children = ( + 1B15613928B7F3D700610B9B /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 1B15614128B7F3D700610B9B /* bitwarden WatchKit App */ = { + isa = PBXGroup; + children = ( + 1B15614228B7F3D800610B9B /* Assets.xcassets */, + ); + path = "bitwarden WatchKit App"; + sourceTree = ""; + }; + 1B15614C28B7F3D800610B9B /* bitwarden WatchKit Extension */ = { + isa = PBXGroup; + children = ( + 1B8BF9072919A2BC006F069E /* Controls */, + 1B5AFF0629197809004478F9 /* Localization */, + 1B59EC5F2900C48300A8718D /* Helpers */, + 1B59EC5A2900BB2900A8718D /* Services */, + 1BDBFEAD290B4591009C78C7 /* DataStorage */, + 1B59EC5429007D7300A8718D /* Entities */, + 1B0A1A9C28E77F4500FF61CD /* Models */, + 1BDBFEA9290851AB009C78C7 /* Views */, + 1BDBFEAA290B41FE009C78C7 /* ViewModels */, + 1B14DF35291186C300EA43F1 /* Utilities */, + 1B15614D28B7F3D800610B9B /* bitwardenApp.swift */, + 1B15615128B7F3D800610B9B /* NotificationController.swift */, + 1B15615328B7F3D800610B9B /* NotificationView.swift */, + 1B15615528B7F3D800610B9B /* ComplicationController.swift */, + 1B15615728B7F3D900610B9B /* Assets.xcassets */, + 1B15615C28B7F3D900610B9B /* Info.plist */, + 1B15615D28B7F3D900610B9B /* PushNotificationPayload.apns */, + 1B15615928B7F3D900610B9B /* Preview Content */, + 1B15616D28B81A4300610B9B /* WatchConnectivityManager.swift */, + ); + path = "bitwarden WatchKit Extension"; + sourceTree = ""; + }; + 1B15615928B7F3D900610B9B /* Preview Content */ = { + isa = PBXGroup; + children = ( + 1B15615A28B7F3D900610B9B /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 1B59EC5429007D7300A8718D /* Entities */ = { + isa = PBXGroup; + children = ( + 1B59EC5529007DEE00A8718D /* BitwardenDB.xcdatamodeld */, + 1B8453EA290C672E00F921E1 /* CipherEntity+CoreDataClass.swift */, + 1B8453EB290C672E00F921E1 /* CipherEntity+CoreDataProperties.swift */, + 1B59EC582900801500A8718D /* StringEncryptionTransformer.swift */, + ); + path = Entities; + sourceTree = ""; + }; + 1B59EC5A2900BB2900A8718D /* Services */ = { + isa = PBXGroup; + children = ( + 1BF5F6DC29103B5F002DDC0C /* Mocks */, + 1B59EC5B2900BB3400A8718D /* CryptoService.swift */, + 1BF5F6DA29103066002DDC0C /* CipherService.swift */, + 1B8BF90A2919AF2A006F069E /* TotpService.swift */, + 1B11C898291BFAB500CE58D8 /* CryptoFunctionService.swift */, + 1BC1CD6229227D3C006540DA /* EnvironmentService.swift */, + 1BD291B8292438830004F33F /* StateService.swift */, + ); + path = Services; + sourceTree = ""; + }; + 1B59EC5F2900C48300A8718D /* Helpers */ = { + isa = PBXGroup; + children = ( + 1B59EC602900C48E00A8718D /* KeychainHelper.swift */, + 1B59EC622901B1C100A8718D /* LoggerHelper.swift */, + 1BC1CD6429227F3C006540DA /* IconImageHelper.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + 1B5AFF0629197809004478F9 /* Localization */ = { + isa = PBXGroup; + children = ( + 1B5AFF0929197822004478F9 /* Localizable.strings */, + ); + path = Localization; + sourceTree = ""; + }; + 1B8BF9072919A2BC006F069E /* Controls */ = { + isa = PBXGroup; + children = ( + 1B8BF9082919A2CC006F069E /* CircularProgressView.swift */, + 1BC1CD6D2922B92B006540DA /* ImageView.swift */, + 1B6BD10129364F020041982D /* AvatarView.swift */, + 1B5F5E37293F9CF8009B5FCC /* TrackableWithHeaderListView.swift */, + ); + path = Controls; + sourceTree = ""; + }; + 1BC1CD6A29229D12006540DA /* Mocks */ = { + isa = PBXGroup; + children = ( + 1BC1CD6B29229D1B006540DA /* CipherMock.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + 1BDBFEA9290851AB009C78C7 /* Views */ = { + isa = PBXGroup; + children = ( + 1B15614F28B7F3D800610B9B /* CipherListView.swift */, + 1B8BF90329199BBB006F069E /* CipherDetailsView.swift */, + 1BD291B22924043C0004F33F /* BWStateView.swift */, + 1B5F5E3D293FBB17009B5FCC /* CipherItemView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 1BDBFEAA290B41FE009C78C7 /* ViewModels */ = { + isa = PBXGroup; + children = ( + 1BDBFEAB290B4215009C78C7 /* CipherListViewModel.swift */, + 1B8BF90529199EC5006F069E /* CipherDetailsViewModel.swift */, + 1BD291B42924047C0004F33F /* BWStateViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + 1BDBFEAD290B4591009C78C7 /* DataStorage */ = { + isa = PBXGroup; + children = ( + 1BDBFEB0290B5BD3009C78C7 /* DBHelperProtocol.swift */, + 1BDBFEB2290B5D07009C78C7 /* CoreDataHelper.swift */, + ); + path = DataStorage; + sourceTree = ""; + }; + 1BF5F6DC29103B5F002DDC0C /* Mocks */ = { + isa = PBXGroup; + children = ( + 1BF5F6DD29103B86002DDC0C /* CipherServiceMock.swift */, + ); + path = Mocks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 1B15612E28B7F3D400610B9B /* bitwarden */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1B15616828B7F3D900610B9B /* Build configuration list for PBXNativeTarget "bitwarden" */; + buildPhases = ( + 1B15612B28B7F3D400610B9B /* Sources */, + 1B15612C28B7F3D400610B9B /* Frameworks */, + 1B15612D28B7F3D400610B9B /* Resources */, + 1B15616728B7F3D900610B9B /* Embed Watch Content */, + ); + buildRules = ( + ); + dependencies = ( + 1B15614028B7F3D700610B9B /* PBXTargetDependency */, + ); + name = bitwarden; + productName = bitwarden; + productReference = 1B15612F28B7F3D400610B9B /* bitwarden.app */; + productType = "com.apple.product-type.application"; + }; + 1B15613C28B7F3D700610B9B /* bitwarden WatchKit App */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1B15616428B7F3D900610B9B /* Build configuration list for PBXNativeTarget "bitwarden WatchKit App" */; + buildPhases = ( + 1B15613B28B7F3D700610B9B /* Resources */, + 1B15616328B7F3D900610B9B /* Embed Foundation Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 1B15614B28B7F3D800610B9B /* PBXTargetDependency */, + ); + name = "bitwarden WatchKit App"; + productName = "bitwarden WatchKit App"; + productReference = 1B15613D28B7F3D700610B9B /* Bitwarden.app */; + productType = "com.apple.product-type.application.watchapp2"; + }; + 1B15614728B7F3D800610B9B /* bitwarden WatchKit Extension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1B15616028B7F3D900610B9B /* Build configuration list for PBXNativeTarget "bitwarden WatchKit Extension" */; + buildPhases = ( + 1B15614428B7F3D800610B9B /* Sources */, + 1B15614528B7F3D800610B9B /* Frameworks */, + 1B15614628B7F3D800610B9B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "bitwarden WatchKit Extension"; + productName = "bitwarden WatchKit Extension"; + productReference = 1B15614828B7F3D800610B9B /* bitwarden WatchKit Extension.appex */; + productType = "com.apple.product-type.watchkit2-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 1B15612728B7F3D400610B9B /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1330; + LastUpgradeCheck = 1400; + TargetAttributes = { + 1B15612E28B7F3D400610B9B = { + CreatedOnToolsVersion = 13.3.1; + }; + 1B15613C28B7F3D700610B9B = { + CreatedOnToolsVersion = 13.3.1; + }; + 1B15614728B7F3D800610B9B = { + CreatedOnToolsVersion = 13.3.1; + }; + }; + }; + buildConfigurationList = 1B15612A28B7F3D400610B9B /* Build configuration list for PBXProject "bitwarden" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 1B15612628B7F3D400610B9B; + productRefGroup = 1B15613028B7F3D400610B9B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1B15612E28B7F3D400610B9B /* bitwarden */, + 1B15613C28B7F3D700610B9B /* bitwarden WatchKit App */, + 1B15614728B7F3D800610B9B /* bitwarden WatchKit Extension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 1B15612D28B7F3D400610B9B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1B15613A28B7F3D700610B9B /* Preview Assets.xcassets in Resources */, + 1B15613728B7F3D700610B9B /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1B15613B28B7F3D700610B9B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1B15614328B7F3D800610B9B /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1B15614628B7F3D800610B9B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1B15615B28B7F3D900610B9B /* Preview Assets.xcassets in Resources */, + 1B5AFF0729197822004478F9 /* Localizable.strings in Resources */, + 1B15615828B7F3D900610B9B /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 1B15612B28B7F3D400610B9B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1B15613528B7F3D400610B9B /* ContentView.swift in Sources */, + 1B0A1A9E28ECD7C400FF61CD /* WatchConnectivityManager.swift in Sources */, + 1B15613328B7F3D400610B9B /* bitwardenApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1B15614428B7F3D800610B9B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1B59EC592900801500A8718D /* StringEncryptionTransformer.swift in Sources */, + 1BD291B32924043C0004F33F /* BWStateView.swift in Sources */, + 1BDBFEAC290B4215009C78C7 /* CipherListViewModel.swift in Sources */, + 1BF5F6DB29103066002DDC0C /* CipherService.swift in Sources */, + 1B5F5E3E293FBB17009B5FCC /* CipherItemView.swift in Sources */, + 1B15616C28B81A2200610B9B /* Cipher.swift in Sources */, + 1B8BF90429199BBB006F069E /* CipherDetailsView.swift in Sources */, + 1B8BF9112919CDBB006F069E /* DateExtensions.swift in Sources */, + 1BD291BB2927E9B50004F33F /* WatchDTO.swift in Sources */, + 1B11C899291BFAB500CE58D8 /* CryptoFunctionService.swift in Sources */, + 1BC1CD6C29229D1B006540DA /* CipherMock.swift in Sources */, + 1B5F5E3A293F9D6F009B5FCC /* ViewExtensions.swift in Sources */, + 1B15615228B7F3D800610B9B /* NotificationController.swift in Sources */, + 1BD291BD292807240004F33F /* User.swift in Sources */, + 1BDBFEB3290B5D07009C78C7 /* CoreDataHelper.swift in Sources */, + 1B15615028B7F3D800610B9B /* CipherListView.swift in Sources */, + 1BD291BF292D0E6F0004F33F /* JsonDecoderExtensions.swift in Sources */, + 1B59EC632901B1C100A8718D /* LoggerHelper.swift in Sources */, + 1BD291C329311E1C0004F33F /* VaultTimeoutAction.swift in Sources */, + 1B15615628B7F3D800610B9B /* ComplicationController.swift in Sources */, + 1BD291B9292438830004F33F /* StateService.swift in Sources */, + 1BC1CD6329227D3C006540DA /* EnvironmentService.swift in Sources */, + 1B8BF9092919A2CC006F069E /* CircularProgressView.swift in Sources */, + 1BD291B7292409410004F33F /* BWState.swift in Sources */, + 1B15614E28B7F3D800610B9B /* bitwardenApp.swift in Sources */, + 1BC1CD6929228CEB006540DA /* StringExtensions.swift in Sources */, + 1BDBFEB1290B5BD3009C78C7 /* DBHelperProtocol.swift in Sources */, + 1B8BF90629199EC5006F069E /* CipherDetailsViewModel.swift in Sources */, + 1BF5F6DE29103B86002DDC0C /* CipherServiceMock.swift in Sources */, + 1BC1CD672922871A006540DA /* URLExtensions.swift in Sources */, + 1B11C89B291C587600CE58D8 /* UInt64Extensions.swift in Sources */, + 1B8453ED290C672E00F921E1 /* CipherEntity+CoreDataProperties.swift in Sources */, + 1BC1CD6E2922B92B006540DA /* ImageView.swift in Sources */, + 1BD291C1292E7E690004F33F /* ErrorExtensions.swift in Sources */, + 1B14DF37291186D900EA43F1 /* EmptyStateViewModifier.swift in Sources */, + 1BD291B52924047C0004F33F /* BWStateViewModel.swift in Sources */, + 1B15616E28B81A4300610B9B /* WatchConnectivityManager.swift in Sources */, + 1B5AFF0329196C81004478F9 /* ColorUtils.swift in Sources */, + 1B59EC612900C48E00A8718D /* KeychainHelper.swift in Sources */, + 1B59EC5729007DEE00A8718D /* BitwardenDB.xcdatamodeld in Sources */, + 1B8BF90D2919BED9006F069E /* Base32.swift in Sources */, + 1B8453EC290C672E00F921E1 /* CipherEntity+CoreDataClass.swift in Sources */, + 1BC1CD6529227F3C006540DA /* IconImageHelper.swift in Sources */, + 1B59EC5C2900BB3400A8718D /* CryptoService.swift in Sources */, + 1B15615428B7F3D800610B9B /* NotificationView.swift in Sources */, + 1B6BD10229364F020041982D /* AvatarView.swift in Sources */, + 1B8BF90B2919AF2A006F069E /* TotpService.swift in Sources */, + 1B5F5E38293F9CF8009B5FCC /* TrackableWithHeaderListView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 1B15614028B7F3D700610B9B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 1B15613C28B7F3D700610B9B /* bitwarden WatchKit App */; + targetProxy = 1B15613F28B7F3D700610B9B /* PBXContainerItemProxy */; + }; + 1B15614B28B7F3D800610B9B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 1B15614728B7F3D800610B9B /* bitwarden WatchKit Extension */; + targetProxy = 1B15614A28B7F3D800610B9B /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 1B5AFF0929197822004478F9 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 1B5AFF0829197822004478F9 /* en */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 1B15615E28B7F3D900610B9B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 1B15615F28B7F3D900610B9B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 1B15616128B7F3D900610B9B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"bitwarden WatchKit Extension/Preview Content\""; + DEVELOPMENT_TEAM = LTZ2PFU5D6; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "bitwarden WatchKit Extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = Bitwarden; + INFOPLIST_KEY_CLKComplicationPrincipalClass = bitwarden_WatchKit_Extension.ComplicationController; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2022.8.1; + PRODUCT_BUNDLE_IDENTIFIER = com.8bit.bitwarden.watchkitapp.watchkitextension; + PRODUCT_NAME = "${TARGET_NAME}"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + VERSIONING_SYSTEM = "apple-generic"; + WATCHOS_DEPLOYMENT_TARGET = 8.0; + }; + name = Debug; + }; + 1B15616228B7F3D900610B9B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; + CODE_SIGN_IDENTITY = "Apple Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"bitwarden WatchKit Extension/Preview Content\""; + DEVELOPMENT_TEAM = LTZ2PFU5D6; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "bitwarden WatchKit Extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = Bitwarden; + INFOPLIST_KEY_CLKComplicationPrincipalClass = bitwarden_WatchKit_Extension.ComplicationController; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2022.8.1; + PRODUCT_BUNDLE_IDENTIFIER = com.8bit.bitwarden.watchkitapp.watchkitextension; + PRODUCT_NAME = "${TARGET_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = "Dist: Bitwarden Watch App Extension"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + WATCHOS_DEPLOYMENT_TARGET = 8.0; + }; + name = Release; + }; + 1B15616528B7F3D900610B9B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = LTZ2PFU5D6; + GENERATE_INFOPLIST_FILE = YES; + IBSC_MODULE = bitwarden_WatchKit_Extension; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.8bit.bitwarden; + MARKETING_VERSION = 2022.8.1; + PRODUCT_BUNDLE_IDENTIFIER = com.8bit.bitwarden.watchkitapp; + PRODUCT_NAME = Bitwarden; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + VERSIONING_SYSTEM = "apple-generic"; + WATCHOS_DEPLOYMENT_TARGET = 8.0; + }; + name = Debug; + }; + 1B15616628B7F3D900610B9B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = LTZ2PFU5D6; + GENERATE_INFOPLIST_FILE = YES; + IBSC_MODULE = bitwarden_WatchKit_Extension; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.8bit.bitwarden; + MARKETING_VERSION = 2022.8.1; + PRODUCT_BUNDLE_IDENTIFIER = com.8bit.bitwarden.watchkitapp; + PRODUCT_NAME = Bitwarden; + PROVISIONING_PROFILE_SPECIFIER = "Dist: Bitwarden Watch App"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + WATCHOS_DEPLOYMENT_TARGET = 8.0; + }; + name = Release; + }; + 1B15616928B7F3D900610B9B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"bitwarden/Preview Content\""; + DEVELOPMENT_TEAM = LTZ2PFU5D6; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 2022.8.1; + PRODUCT_BUNDLE_IDENTIFIER = com.8bit.bitwarden; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 1B15616A28B7F3D900610B9B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"bitwarden/Preview Content\""; + DEVELOPMENT_TEAM = LTZ2PFU5D6; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 2022.8.1; + PRODUCT_BUNDLE_IDENTIFIER = com.8bit.bitwarden; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Dist: Bitwarden 2021"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1B15612A28B7F3D400610B9B /* Build configuration list for PBXProject "bitwarden" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1B15615E28B7F3D900610B9B /* Debug */, + 1B15615F28B7F3D900610B9B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1B15616028B7F3D900610B9B /* Build configuration list for PBXNativeTarget "bitwarden WatchKit Extension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1B15616128B7F3D900610B9B /* Debug */, + 1B15616228B7F3D900610B9B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1B15616428B7F3D900610B9B /* Build configuration list for PBXNativeTarget "bitwarden WatchKit App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1B15616528B7F3D900610B9B /* Debug */, + 1B15616628B7F3D900610B9B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1B15616828B7F3D900610B9B /* Build configuration list for PBXNativeTarget "bitwarden" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1B15616928B7F3D900610B9B /* Debug */, + 1B15616A28B7F3D900610B9B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + 1B59EC5529007DEE00A8718D /* BitwardenDB.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + 1B59EC5629007DEE00A8718D /* BitwardenDB.xcdatamodel */, + ); + currentVersion = 1B59EC5629007DEE00A8718D /* BitwardenDB.xcdatamodel */; + path = BitwardenDB.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ + }; + rootObject = 1B15612728B7F3D400610B9B /* Project object */; +} diff --git a/src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App (Complication).xcscheme b/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App (Complication).xcscheme new file mode 100644 index 000000000..3b8c13b18 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App (Complication).xcscheme @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App (Notification).xcscheme b/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App (Notification).xcscheme new file mode 100644 index 000000000..aafffd87d --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App (Notification).xcscheme @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App.xcscheme b/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App.xcscheme new file mode 100644 index 000000000..2437681c1 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App.xcscheme @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden.xcscheme b/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden.xcscheme new file mode 100644 index 000000000..0023047d2 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/watchOS/bitwarden/bitwarden/Assets.xcassets/AccentColor.colorset/Contents.json b/src/watchOS/bitwarden/bitwarden/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/watchOS/bitwarden/bitwarden/Assets.xcassets/AppIcon.appiconset/Contents.json b/src/watchOS/bitwarden/bitwarden/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..9221b9bb1 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/watchOS/bitwarden/bitwarden/Assets.xcassets/Contents.json b/src/watchOS/bitwarden/bitwarden/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/watchOS/bitwarden/bitwarden/ContentView.swift b/src/watchOS/bitwarden/bitwarden/ContentView.swift new file mode 100644 index 000000000..9bcc4c32f --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden/ContentView.swift @@ -0,0 +1,27 @@ +import SwiftUI + +struct ContentView: View { + @ObservedObject private var connectivityManager = WatchConnectivityManager.shared + + var body: some View { + VStack{ + Button("Main Tap me!", action: { + WatchConnectivityManager.shared.send("From main app") + }) +// List(viewModel.ciphers){ cipher in +// Text("\(cipher.name): \(cipher.login.totp)") +// .padding() +// } + } + .alert(item: $connectivityManager.notificationMessage) { message in + Alert(title: Text(message.text), + dismissButton: .default(Text("Dismiss"))) + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/src/watchOS/bitwarden/bitwarden/Preview Content/Preview Assets.xcassets/Contents.json b/src/watchOS/bitwarden/bitwarden/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/watchOS/bitwarden/bitwarden/WatchConnectivityManager.swift b/src/watchOS/bitwarden/bitwarden/WatchConnectivityManager.swift new file mode 100644 index 000000000..9858b5aa9 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden/WatchConnectivityManager.swift @@ -0,0 +1,126 @@ +import Foundation +import WatchConnectivity + +struct NotificationMessage: Identifiable { + let id = UUID() + let text: String +} + +final class WatchConnectivityManager: NSObject, ObservableObject { + static let shared = WatchConnectivityManager() + @Published var notificationMessage: NotificationMessage? = nil + + private let kMessageKey = "message" + private let kCipherDataKey = "cipherData" + + private override init() { + super.init() + + if WCSession.isSupported() { + WCSession.default.delegate = self + WCSession.default.activate() + } + } + + func send(_ message: String) { + guard WCSession.default.activationState == .activated else { + return + } + #if os(iOS) + guard WCSession.default.isWatchAppInstalled else { + return + } + #else + guard WCSession.default.isCompanionAppInstalled else { + return + } + #endif + + guard WCSession.default.isReachable else { + return + } + + WCSession.default.sendMessage([kMessageKey : message], replyHandler: nil) { error in + print("Cannot send message: \(String(describing: error))") + } + } +} + +extension WatchConnectivityManager: WCSessionDelegate { + func session(_ session: WCSession, didReceiveMessage message: [String : Any]) { + DispatchQueue.main.async { [weak self] in + self?.notificationMessage = NotificationMessage(text: "testing this didReceiveMessage") + } + + if let notificationText = message[kMessageKey] as? String { + DispatchQueue.main.async { [weak self] in + self?.notificationMessage = NotificationMessage(text: notificationText) + } + } + } + + func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { + DispatchQueue.main.async { [weak self] in + self?.notificationMessage = NotificationMessage(text: "testing this didReceiveMessage") + } + let returnMessage: [String : Any] = [ + "key1" : "s" + ] + + replyHandler(returnMessage) + +// if let notificationText = message[kMessageKey] as? String { +// DispatchQueue.main.async { [weak self] in +// self?.notificationMessage = NotificationMessage(text: notificationText) +// } +// } + + } + + func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) { + DispatchQueue.main.async { [weak self] in + self?.notificationMessage = NotificationMessage(text: "testing this didReceiveUserInfo") + } + } + + func session(_ session: WCSession, + activationDidCompleteWith activationState: WCSessionActivationState, + error: Error?) { + DispatchQueue.main.async { [weak self] in + self?.notificationMessage = NotificationMessage(text: "testing this activationDidCompleteWith") + } + + } + + func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) { + DispatchQueue.main.async { [weak self] in + self?.notificationMessage = NotificationMessage(text: "testing this didReceiveApplicationContext") + } + if let notificationText = applicationContext[kCipherDataKey] as? String { + +// let decoder = JSONDecoder() +// do { +// let ciphers = try decoder.decode(Cipher.self, from: notificationText.data(using: .utf8)!) +// + DispatchQueue.main.async { [weak self] in + let index1 = notificationText.index(notificationText.startIndex, offsetBy: 0) + let index2 = notificationText.index(notificationText.startIndex, offsetBy: 6) + let indexRange = index1...index2 + let subString = notificationText[indexRange] // eil + + self?.notificationMessage = NotificationMessage(text: String(subString)) + } +// } +// catch { +// print(error) +// } + } + } + + #if os(iOS) + func sessionDidBecomeInactive(_ session: WCSession) {} + func sessionDidDeactivate(_ session: WCSession) { + session.activate() + } + #endif +} diff --git a/src/watchOS/bitwarden/bitwarden/bitwardenApp.swift b/src/watchOS/bitwarden/bitwarden/bitwardenApp.swift new file mode 100644 index 000000000..71f943c26 --- /dev/null +++ b/src/watchOS/bitwarden/bitwarden/bitwardenApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct bitwardenApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +}