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 <joseph.s.flinn@gmail.com>

* [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 <fedemkr@gmail.com>
Co-authored-by: Joseph Flinn <joseph.s.flinn@gmail.com>
This commit is contained in:
Álison Fernandes 2022-12-07 16:39:20 +00:00 committed by GitHub
parent 2a60ff62d8
commit fa6bac3b43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
145 changed files with 5625 additions and 21 deletions

View File

@ -14,6 +14,10 @@
<string>Dist: Extension 2021</string>
<key>com.8bit.bitwarden.share-extension</key>
<string>Dist: Share Extension 2021</string>
<key>com.8bit.bitwarden.watchkitapp</key>
<string>Dist: Bitwarden Watch App</string>
<key>com.8bit.bitwarden.watchkitapp.watchkitextension</key>
<string>Dist: Bitwarden Watch App Extension</string>
</dict>
</dict>
</plist>

Binary file not shown.

Binary file not shown.

View File

@ -58,6 +58,7 @@ jobs:
android:
name: Android
runs-on: windows-2022
if: github.repository == 'putting here a false condition so that this doesnt run while testing iOS CI'
needs: setup
strategy:
fail-fast: false
@ -258,6 +259,7 @@ jobs:
f-droid:
name: F-Droid Build
runs-on: windows-2022
if: github.repository == 'putting here a false condition so that this doesnt run while testing iOS CI'
steps:
- name: Setup NuGet
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
@ -497,6 +499,12 @@ jobs:
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output $HOME/secrets/dist_share_extension.mobileprovision \
./.github/secrets/dist_share_extension.mobileprovision.gpg
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output $HOME/secrets/dist_watch_app.mobileprovision \
./.github/secrets/dist_watch_app.mobileprovision.gpg
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output $HOME/secrets/dist_watch_app_extension.mobileprovision \
./.github/secrets/dist_watch_app_extension.mobileprovision.gpg
shell: bash
- name: Increment version
@ -511,6 +519,9 @@ jobs:
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.Extension/Info.plist
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.Autofill/Info.plist
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$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

125
.gitignore vendored
View File

@ -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

View File

@ -159,6 +159,7 @@
<Compile Include="Services\AutofillHandler.cs" />
<Compile Include="Constants.cs" />
<Compile Include="Effects\RemoveFontPaddingEffect.cs" />
<Compile Include="Services\WatchDeviceService.cs" />
</ItemGroup>
<ItemGroup>
<AndroidAsset Include="Assets\bwi-font.ttf" />

View File

@ -45,9 +45,16 @@ namespace Bit.Droid
if (ServiceContainer.RegisteredServices.Count == 0)
{
RegisterLocalServices();
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
ServiceContainer.Init(deviceActionService.DeviceUserAgent, Core.Constants.ClearCiphersCacheKey,
Core.Constants.AndroidAllClearCipherCacheKeys);
ServiceContainer.Register<IWatchDeviceService>(new WatchDeviceService(ServiceContainer.Resolve<ICipherService>(),
ServiceContainer.Resolve<IEnvironmentService>(),
ServiceContainer.Resolve<IStateService>(),
ServiceContainer.Resolve<IVaultTimeoutService>()));
InitializeAppSetup();
// TODO: Update when https://github.com/bitwarden/mobile/pull/1662 gets merged
@ -73,8 +80,9 @@ namespace Bit.Droid
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IAuthService>("authService"),
ServiceContainer.Resolve<ILogger>("logger"),
ServiceContainer.Resolve<IMessagingService>("messagingService"));
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
ServiceContainer.Resolve<IMessagingService>("messagingService"),
ServiceContainer.Resolve<IWatchDeviceService>());
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
}
#if !FDROID
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)

View File

@ -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
{

View File

@ -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();
}
}

View File

@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Models;
namespace Bit.App.Abstractions
{

View File

@ -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<int?> _secretEntryFocusWeakEventManager = new WeakEventManager<int?>();
private string _email;
@ -56,6 +57,7 @@ namespace Bit.App.Pages
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
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();
}

View File

@ -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<KeyValuePair<string, int?>> _vaultTimeouts =
new List<KeyValuePair<string, int?>>
@ -87,6 +92,7 @@ namespace Bit.App.Pages
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
_loggerService = ServiceContainer.Resolve<ILogger>("logger");
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
GroupedItems = new ObservableRangeCollection<ISettingsPageListItem>();
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<SettingsPageListItem>
var accountItems = new List<SettingsPageListItem>();
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();
}
}
}

View File

@ -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<ICustomFieldItemFactory>("customFieldItemFactory");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
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

View File

@ -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<ICustomFieldItemViewModel> _fields;
private bool _canAccessPremium;
@ -62,6 +63,7 @@ namespace Bit.App.Pages
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
_customFieldItemFactory = ServiceContainer.Resolve<ICustomFieldItemFactory>("customFieldItemFactory");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
CopyCommand = new AsyncCommand<string>((id) => CopyAsync(id, null), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
CopyUriCommand = new AsyncCommand<LoginUriView>(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);

View File

@ -1498,6 +1498,15 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Connect to Watch.
/// </summary>
public static string ConnectToWatch {
get {
return ResourceManager.GetString("ConnectToWatch", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Continue.
/// </summary>

View File

@ -2453,6 +2453,9 @@ select Add TOTP to store the key safely</value>
<data name="Random" xml:space="preserve">
<value>Random</value>
</data>
<data name="ConnectToWatch" xml:space="preserve">
<value>Connect to Watch</value>
</data>
<data name="AccessibilityServiceDisclosure" xml:space="preserve">
<value>Accessibility Service Disclosure</value>
</data>

View File

@ -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<WatchState> 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();
}
}

View File

@ -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<AppOptions> _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);
});
}

View File

@ -19,7 +19,7 @@ namespace Bit.Core.Abstractions
Task DeleteWithServerAsync(string id);
Task<Cipher> EncryptAsync(CipherView model, SymmetricCryptoKey key = null, Cipher originalCipher = null);
Task<List<Cipher>> GetAllAsync();
Task<List<CipherView>> GetAllDecryptedAsync();
Task<List<CipherView>> GetAllDecryptedAsync(Func<Cipher, bool> filter = null);
Task<Tuple<List<CipherView>, List<CipherView>, List<CipherView>>> GetAllDecryptedByUrlAsync(string url,
List<CipherType> includeOtherTypes = null);
Task<List<CipherView>> GetAllDecryptedForGroupingAsync(string groupingId, bool folder = true);

View File

@ -14,6 +14,7 @@ namespace Bit.Core.Abstractions
List<AccountView> AccountViews { get; }
Task<string> GetActiveUserIdAsync();
Task<string> GetActiveUserEmailAsync();
Task<T> GetActiveUserCustomDataAsync<T>(Func<Account, T> dataMapper);
Task<bool> IsActiveAccountAsync(string userId = null);
Task SetActiveUserAsync(string userId);
Task CheckExtensionActiveUserAndSwitchIfNeededAsync();
@ -159,5 +160,7 @@ namespace Bit.Core.Abstractions
Task SetPasswordlessLoginNotificationAsync(PasswordlessRequestNotification value);
Task<UsernameGenerationOptions> GetUsernameGenerationOptionsAsync(string userId = null);
Task SetUsernameGenerationOptionsAsync(UsernameGenerationOptions value, string userId = null);
Task<bool> GetShouldConnectToWatchAsync(string userId = null);
Task SetShouldConnectToWatchAsync(bool shouldConnect, string userId = null);
}
}

View File

@ -0,0 +1,12 @@
using System.Threading.Tasks;
namespace Bit.Core.Abstractions
{
public interface IWatchDeviceService
{
bool IsConnected { get; }
Task SetShouldConnectToWatchAsync(bool shouldConnectToWatch);
Task SyncDataToWatchAsync();
}
}

View File

@ -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}";
}
}

View File

@ -0,0 +1,13 @@
namespace Bit.Core.Enums
{
public enum WatchState : byte
{
Valid = 0,
NeedLogin,
NeedPremium,
NeedSetup,
Need2FAItem,
Syncing
//NeedUnlock
}
}

View File

@ -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<SimpleLoginUriView> Uris { get; set; }
}
public class SimpleLoginUriView
{
public SimpleLoginUriView(string uri)
{
Uri = uri;
}
public string Uri { get; set; }
}
}

View File

@ -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<SimpleCipherView> 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; }
//}
}
}

View File

@ -26,6 +26,8 @@ namespace Bit.Core.Services
private readonly IKeyConnectorService _keyConnectorService;
private readonly IPasswordGenerationService _passwordGenerationService;
private readonly bool _setCryptoKeys;
private readonly LazyResolve<IWatchDeviceService> _watchDeviceService = new LazyResolve<IWatchDeviceService>();
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<TwoFactorProvider> GetSupportedTwoFactorProviders()

View File

@ -226,7 +226,7 @@ namespace Bit.Core.Services
return response?.ToList() ?? new List<Cipher>();
}
public async Task<List<CipherView>> GetAllDecryptedAsync()
public async Task<List<CipherView>> GetAllDecryptedAsync(Func<Cipher, bool> 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<Task>();
var ciphers = await GetAllAsync();
IEnumerable<Cipher> 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;
}

View File

@ -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<T> GetActiveUserCustomDataAsync<T>(Func<Account, T> dataMapper)
{
var userId = await GetActiveUserIdAsync();
var account = await GetAccountAsync(
ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync())
);
return dataMapper(account);
}
public async Task<bool> 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<bool> GetShouldConnectToWatchAsync(string userId = null)
{
var reconciledOptions =
ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync());
var key = Constants.ShouldConnectToWatchKey(reconciledOptions.UserId);
return await GetValueAsync<bool?>(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);
}
}
}

View File

@ -27,6 +27,8 @@ namespace Bit.Core.Services
private readonly ILogger _logger;
private readonly Func<Tuple<string, bool, bool>, Task> _logoutCallbackAsync;
private readonly LazyResolve<IWatchDeviceService> _watchDeviceService = new LazyResolve<IWatchDeviceService>();
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

View File

@ -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<string, object>
{
[$"watchDto-{DateTime.UtcNow.ToLongTimeString()}"] = serializedData
});
return Task.CompletedTask;
}
protected override void ConnectToWatch()
{
WCSessionManager.SharedManager.StartSession();
}
}
}

View File

@ -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<NSString, NSObject> ToNSDictionary(this Dictionary<string, object> dict)
{
return dict.ToNSDictionary(k => new NSString(k), v => (NSObject)new NSString(JsonConvert.SerializeObject(v)));
}
public static NSDictionary<KTo,VTo> ToNSDictionary<KFrom,VFrom,KTo,VTo>(this Dictionary<KFrom, VFrom> dict, Func<KFrom, KTo> keyConverter, Func<VFrom, VTo> 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<KTo, VTo>.FromObjectsAndKeys(NSValues, NSKeys, NSKeys.Count());
}
}
}

View File

@ -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<string, object> 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<string, object> 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<string, object> 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<NSString, NSObject> 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<NSString, NSObject> 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
}
}

View File

@ -48,6 +48,12 @@ namespace Bit.iOS.Core.Utilities
clearCipherCacheKey,
Bit.Core.Constants.iOSAllClearCipherCacheKeys);
InitLogger();
ServiceContainer.Register<IWatchDeviceService>(new WatchDeviceService(ServiceContainer.Resolve<ICipherService>(),
ServiceContainer.Resolve<IEnvironmentService>(),
ServiceContainer.Resolve<IStateService>(),
ServiceContainer.Resolve<IVaultTimeoutService>()));
Bootstrap();
var appOptions = new AppOptions { IosExtension = true };
@ -226,7 +232,8 @@ namespace Bit.iOS.Core.Utilities
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IAuthService>("authService"),
ServiceContainer.Resolve<ILogger>("logger"),
ServiceContainer.Resolve<IMessagingService>("messagingService"));
ServiceContainer.Resolve<IMessagingService>("messagingService"),
ServiceContainer.Resolve<IWatchDeviceService>());
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
if (postBootstrapFunc != null)

View File

@ -204,9 +204,12 @@
<Compile Include="Renderers\CollectionView\CollectionException.cs" />
<Compile Include="Renderers\CollectionView\ExtendedGroupableItemsViewDelegator.cs" />
<Compile Include="Effects\NoEmojiKeyboardEffect.cs" />
<Compile Include="Utilities\WCSessionManager.cs" />
<Compile Include="Services\FileService.cs" />
<Compile Include="Utilities\UIViewControllerExtensions.cs" />
<Compile Include="Services\AutofillHandler.cs" />
<Compile Include="Utilities\DictionaryExtensions.cs" />
<Compile Include="Services\WatchDeviceService.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\App\App.csproj">

View File

@ -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<IWatchDeviceService>(new WatchDeviceService(ServiceContainer.Resolve<ICipherService>(),
ServiceContainer.Resolve<IEnvironmentService>(),
ServiceContainer.Resolve<IStateService>(),
ServiceContainer.Resolve<IVaultTimeoutService>()));
_pushHandler = new iOSPushNotificationHandler(
ServiceContainer.Resolve<IPushNotificationListenerService>("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();
}
}
}
}

View File

@ -134,6 +134,15 @@
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Default' ">
<AppExtensionDebugBundleId />
</PropertyGroup>
<PropertyGroup>
<WatchAppBuildPath Condition=" '$(Configuration)' == 'Debug' ">$(Home)/Library/Developer/Xcode/DerivedData/bitwarden-cbtqsueryycvflfzbsoteofskiyr/Build/Products</WatchAppBuildPath>
<WatchAppBuildPath Condition=" '$(Configuration)' != 'Debug' ">$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..'))/watchOS/bitwarden.xcarchive/Products/Applications/bitwarden.app/Watch</WatchAppBuildPath>
<WatchAppBundle>Bitwarden.app</WatchAppBundle>
<WatchAppConfiguration Condition=" '$(Platform)' == 'iPhoneSimulator' ">watchsimulator</WatchAppConfiguration>
<WatchAppConfiguration Condition=" '$(Platform)' == 'iPhone' ">watchos</WatchAppConfiguration>
<WatchAppBundleFullPath Condition=" '$(Configuration)' == 'Debug' ">$(WatchAppBuildPath)/$(Configuration)-$(WatchAppConfiguration)/$(WatchAppBundle)</WatchAppBundleFullPath>
<WatchAppBundleFullPath Condition=" '$(Configuration)' != 'Debug' ">$(WatchAppBuildPath)/$(WatchAppBundle)</WatchAppBundleFullPath>
</PropertyGroup>
<ItemGroup>
<Compile Include="Main.cs" />
<Compile Include="AppDelegate.cs" />
@ -406,4 +415,17 @@
<Folder Include="Resources\Assets.xcassets\LaunchScreen.imageset\" />
<Folder Include="Resources\Assets.xcassets\ic_warning.imageset\" />
</ItemGroup>
<ItemGroup Condition=" '$(Configuration)' == 'Debug' AND Exists('$(WatchAppBundleFullPath)') ">
<_ResolvedWatchAppReferences Include="$(WatchAppBundleFullPath)" />
</ItemGroup>
<ItemGroup Condition=" '$(Configuration)' != 'Debug' ">
<_ResolvedWatchAppReferences Include="$(WatchAppBundleFullPath)" />
</ItemGroup>
<PropertyGroup Condition=" '$(_ResolvedWatchAppReferences)' != '' ">
<CodesignExtraArgs>--deep</CodesignExtraArgs>
</PropertyGroup>
<Target Name="PrintWatchAppBundleStatus" BeforeTargets="Build">
<Message Text="WatchAppBundleFullPath: '$(WatchAppBundleFullPath)' exists" Condition=" Exists('$(WatchAppBundleFullPath)') " />
<Message Text="WatchAppBundleFullPath: '$(WatchAppBundleFullPath)' does NOT exist" Condition=" !Exists('$(WatchAppBundleFullPath)') " />
</Target>
</Project>

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "logo-horizontal-blue (2) 3.pdf",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "logo-horizontal-blue (2) 2.pdf",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "logo-horizontal-blue (2) 1.pdf",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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"
}
}

View File

@ -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
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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
}
}

View File

@ -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
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -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
}
}

View File

@ -0,0 +1,22 @@
<svg width="209" height="147" viewBox="0 0 209 147" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M91.5353 144.346H154.59" stroke="#89929F" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M111.803 126.331V144.346" stroke="#89929F" stroke-width="4" stroke-linejoin="round"/>
<path d="M132.071 126.331V144.346" stroke="#89929F" stroke-width="4" stroke-linejoin="round"/>
<rect x="51" y="34" width="143" height="92.3307" rx="4" fill="#1F242E" stroke="#89929F" stroke-width="4"/>
<rect x="62.2598" y="44.1339" width="121.606" height="72.063" rx="2" fill="#2F343D" stroke="#89929F" stroke-width="2"/>
<path d="M177 56H145" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M177 68H155" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M145 68H130" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M177 80H163" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M152 80H121" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M177 92H145" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M134 92H117" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M177 104H132" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M121 104H95" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M84 104H68" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M32.4069 50.6362C32.4069 77.4973 54.4612 99.2725 81.6667 99.2725C108.872 99.2725 130.927 77.4973 130.927 50.6362C130.927 23.7752 108.872 2 81.6667 2C54.4613 2 32.4069 23.7752 32.4069 50.6362Z" fill="#1F242E" stroke="#89929F" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.53554 127.318L5.91014 129.692C7.86276 131.645 11.0286 131.645 12.9812 129.692L42.7595 99.9139L50.5328 92.1407C51.2151 91.4515 51.063 90.3025 50.2643 89.7524C47.0256 87.5217 44.4776 84.7321 43.0358 82.8551C42.5613 82.2373 41.638 82.1441 41.0871 82.695L33.3139 90.4682L3.53553 120.247C1.58291 122.199 1.58291 125.365 3.53554 127.318Z" fill="#2F343D" stroke="#89929F" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<rect width="66" height="19" rx="4" transform="matrix(-1 0 0 1 115 24)" fill="#2F343D" stroke="#89929F" stroke-width="4"/>
<path d="M85.2681 68V78.4644C85.2681 80.6735 83.4772 82.4644 81.2681 82.4644H46.2868C45.1497 82.4644 44.0595 81.9865 43.3321 81.1126C40.6843 77.9313 38.2669 74.1839 36.0797 69.2564C34.96 66.7339 36.8938 64 39.6536 64H81.2681C83.4772 64 85.2681 65.7909 85.2681 68Z" fill="#2F343D" stroke="#89929F" stroke-width="4" stroke-linecap="round"/>
<path d="M114 79.1012V68C114 65.7909 115.791 64 118 64H123.95C126.681 64 128.499 66.6372 127.261 69.0711C125.207 73.1122 122.318 77.9136 119.981 81.1559C119.376 81.994 118.396 82.4644 117.363 82.4644C115.506 82.4644 114 80.9586 114 79.1012Z" fill="#2F343D" stroke="#89929F" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,22 @@
<svg width="209" height="147" viewBox="0 0 209 147" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M91.5353 144.346H154.59" stroke="#89929F" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M111.803 126.331V144.346" stroke="#89929F" stroke-width="4" stroke-linejoin="round"/>
<path d="M132.071 126.331V144.346" stroke="#89929F" stroke-width="4" stroke-linejoin="round"/>
<rect x="51" y="34" width="143" height="92.3307" rx="4" fill="#1F242E" stroke="#89929F" stroke-width="4"/>
<rect x="62.2598" y="44.1339" width="121.606" height="72.063" rx="2" fill="#2F343D" stroke="#89929F" stroke-width="2"/>
<path d="M177 56H145" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M177 68H155" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M145 68H130" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M177 80H163" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M152 80H121" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M177 92H145" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M134 92H117" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M177 104H132" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M121 104H95" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M84 104H68" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M32.4069 50.6362C32.4069 77.4973 54.4612 99.2725 81.6667 99.2725C108.872 99.2725 130.927 77.4973 130.927 50.6362C130.927 23.7752 108.872 2 81.6667 2C54.4613 2 32.4069 23.7752 32.4069 50.6362Z" fill="#1F242E" stroke="#89929F" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.53554 127.318L5.91014 129.692C7.86276 131.645 11.0286 131.645 12.9812 129.692L42.7595 99.9139L50.5328 92.1407C51.2151 91.4515 51.063 90.3025 50.2643 89.7524C47.0256 87.5217 44.4776 84.7321 43.0358 82.8551C42.5613 82.2373 41.638 82.1441 41.0871 82.695L33.3139 90.4682L3.53553 120.247C1.58291 122.199 1.58291 125.365 3.53554 127.318Z" fill="#2F343D" stroke="#89929F" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<rect width="66" height="19" rx="4" transform="matrix(-1 0 0 1 115 24)" fill="#2F343D" stroke="#89929F" stroke-width="4"/>
<path d="M85.2681 68V78.4644C85.2681 80.6735 83.4772 82.4644 81.2681 82.4644H46.2868C45.1497 82.4644 44.0595 81.9865 43.3321 81.1126C40.6843 77.9313 38.2669 74.1839 36.0797 69.2564C34.96 66.7339 36.8938 64 39.6536 64H81.2681C83.4772 64 85.2681 65.7909 85.2681 68Z" fill="#2F343D" stroke="#89929F" stroke-width="4" stroke-linecap="round"/>
<path d="M114 79.1012V68C114 65.7909 115.791 64 118 64H123.95C126.681 64 128.499 66.6372 127.261 69.0711C125.207 73.1122 122.318 77.9136 119.981 81.1559C119.376 81.994 118.396 82.4644 117.363 82.4644C115.506 82.4644 114 80.9586 114 79.1012Z" fill="#2F343D" stroke="#89929F" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,22 @@
<svg width="209" height="147" viewBox="0 0 209 147" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M91.5353 144.346H154.59" stroke="#89929F" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M111.803 126.331V144.346" stroke="#89929F" stroke-width="4" stroke-linejoin="round"/>
<path d="M132.071 126.331V144.346" stroke="#89929F" stroke-width="4" stroke-linejoin="round"/>
<rect x="51" y="34" width="143" height="92.3307" rx="4" fill="#1F242E" stroke="#89929F" stroke-width="4"/>
<rect x="62.2598" y="44.1339" width="121.606" height="72.063" rx="2" fill="#2F343D" stroke="#89929F" stroke-width="2"/>
<path d="M177 56H145" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M177 68H155" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M145 68H130" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M177 80H163" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M152 80H121" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M177 92H145" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M134 92H117" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M177 104H132" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M121 104H95" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M84 104H68" stroke="#89929F" stroke-width="2" stroke-linecap="round"/>
<path d="M32.4069 50.6362C32.4069 77.4973 54.4612 99.2725 81.6667 99.2725C108.872 99.2725 130.927 77.4973 130.927 50.6362C130.927 23.7752 108.872 2 81.6667 2C54.4613 2 32.4069 23.7752 32.4069 50.6362Z" fill="#1F242E" stroke="#89929F" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.53554 127.318L5.91014 129.692C7.86276 131.645 11.0286 131.645 12.9812 129.692L42.7595 99.9139L50.5328 92.1407C51.2151 91.4515 51.063 90.3025 50.2643 89.7524C47.0256 87.5217 44.4776 84.7321 43.0358 82.8551C42.5613 82.2373 41.638 82.1441 41.0871 82.695L33.3139 90.4682L3.53553 120.247C1.58291 122.199 1.58291 125.365 3.53554 127.318Z" fill="#2F343D" stroke="#89929F" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<rect width="66" height="19" rx="4" transform="matrix(-1 0 0 1 115 24)" fill="#2F343D" stroke="#89929F" stroke-width="4"/>
<path d="M85.2681 68V78.4644C85.2681 80.6735 83.4772 82.4644 81.2681 82.4644H46.2868C45.1497 82.4644 44.0595 81.9865 43.3321 81.1126C40.6843 77.9313 38.2669 74.1839 36.0797 69.2564C34.96 66.7339 36.8938 64 39.6536 64H81.2681C83.4772 64 85.2681 65.7909 85.2681 68Z" fill="#2F343D" stroke="#89929F" stroke-width="4" stroke-linecap="round"/>
<path d="M114 79.1012V68C114 65.7909 115.791 64 118 64H123.95C126.681 64 128.499 66.6372 127.261 69.0711C125.207 73.1122 122.318 77.9136 119.981 81.1559C119.376 81.994 118.396 82.4644 117.363 82.4644C115.506 82.4644 114 80.9586 114 79.1012Z" fill="#2F343D" stroke="#89929F" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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..<charCount {
text += parts[i].prefix(1);
}
return text;
}
if sanitizedData.count > 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"))
}
}

View File

@ -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)
}
}

View File

@ -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<PlaceholderView: View>: 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<NSString, UIImage>()
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
}
}

View File

@ -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<HeaderContent:View, Content: View>: 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
}
}
}

View File

@ -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<T: NSManagedObject>(_ objectType: T.Type, _ entityName: String, predicate: NSPredicate? = nil, limit: Int? = nil) -> Result<[T], Error> {
let request = NSFetchRequest<T>(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<T: NSManagedObject>(_ objectType: T.Type, predicate: NSPredicate?) -> Result<T?, Error> {
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<NSFetchRequestResult> = 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()
}
}
}

View File

@ -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<ObjectType?, Error>
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)
}
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21512" systemVersion="22A380" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="CipherEntity" representedClassName="CipherEntity" syncable="YES">
<attribute name="id" optional="YES" attributeType="String" valueTransformerName="StringEncryptionTransformer"/>
<attribute name="loginUris" optional="YES" attributeType="Transformable" valueTransformerName="StringEncryptionTransformer"/>
<attribute name="name" optional="YES" attributeType="Transformable" valueTransformerName="StringEncryptionTransformer"/>
<attribute name="totp" optional="YES" attributeType="Transformable" valueTransformerName="StringEncryptionTransformer"/>
<attribute name="type" optional="YES" attributeType="Transformable" valueTransformerName="StringEncryptionTransformer"/>
<attribute name="userId" optional="YES" attributeType="String" valueTransformerName="StringEncryptionTransformer"/>
<attribute name="username" optional="YES" attributeType="Transformable" valueTransformerName="StringEncryptionTransformer"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
</model>

View File

@ -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")!
}

View File

@ -0,0 +1,34 @@
import Foundation
import CoreData
extension CipherEntity {
@nonobjc public class func fetchRequest() -> NSFetchRequest<CipherEntity> {
return NSFetchRequest<CipherEntity>(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))
}
}

View File

@ -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)
}
}

View File

@ -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"
}
}

View File

@ -0,0 +1,94 @@
import Foundation
final class KeychainHelper {
static let standard = KeychainHelper()
let genericService = "com.8bit.bitwarden.watch.kc"
private init() {}
func read<T>(_ 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<T>(_ 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)
}
}

View File

@ -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)
}
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>WKAppBundleIdentifier</key>
<string>com.8bit.bitwarden.watchkitapp</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.watchkit</string>
</dict>
</dict>
</plist>

View File

@ -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";

View File

@ -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
}
}

View File

@ -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")
]
}

View File

@ -0,0 +1,7 @@
import Foundation
struct User : Codable {
var id: String
var email: String?
var name: String?
}

View File

@ -0,0 +1,6 @@
//import Foundation
//
//enum VaultTimeoutAction : Int, Codable {
// case lock = 0
// case logout = 1
//}

View File

@ -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
//}

Some files were not shown because too many files have changed in this diff Show More