Skip to main content

watchOS

Overall architecture

The watchOS application is organized as follows:

  • src/watchOS: All the code specific to the watchOS platform
    • bitwarden: Stub iOS app so that the watchOS app has a companion app on XCode
    • bitwarden WatchKit App: Main Watch app where we set assets.
    • bitwarden WatchKit Extension: All the logic and presentation logic for the Watch app is here

So almost all the things related to the watch app will be in the WatchKit Extension, the WatchKit App one will be only for assets and some configs.

Then in the Extension we have a layered architecture:

  • State (it's a really simplified version of the iOS state)
  • Persistence (here we use CoreData to interact with the Database)
  • Services (totp generation, crypto services and business logic)
  • Presentation (use SwiftUI for the UI with an MVVM pattern)

Integration with iOS

The watchOS app is developed using XCode and Swift and we need to integrate it to the .NET MAUI iOS application.

For this, the iOS.csproj has been adapted taking a solution provided in the Xamarin.Forms GitHub repository and modified to our needs:

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

So on the PropertyGroup the WatchAppBundleFullPath is assembled together depending on the Configuration and the Platform taking the output of the XCode watchOS app build. Then there are some ItemGroup to include the watch app depending on if it exists and the Configuration. The task _ResolvedWatchAppReferences is the one responsible to peek into the Bitwarden.app built by XCode and if it finds a Watch app, it will just bundle it to the Xamarin iOS application. Finally, if the Watch app is bundled, deep signing is enabled and the build path is printed.

caution

As one can see in the csproj, to bundle the watchOS app into the iOS app one needs to target the correct platform. So if one is going to use a device, target the device on XCode to build the watchOS app and after the build is done one can go to VS4M to build the iOS app (which will bundle the watchOS one) and run it on the device.

Synchronization between iPhone and Watch

In order to sync data between the iPhone and the Watch apps the Watch Connectivity Framework is used.

So there is a Watch Connectivity Manager on each side that is the interface used for the services on each platform to communicate.

For the sync communication, mainly updateApplicationContext is used given that it always have the latest data sent available, it's sent in the background and the counterpart device doesn't necessarily needs to be in range (so it's cached until it can be delivered). Additionally, sendMessage is also used to signal the counterpart of some action to take quickly (like triggering a sync from the Watch).

The WatchDTO is the object that is sent in the synchronization that has all the information for the Watch.

plantuml

plantuml

States

The next ones are the states in which the Watch application can be at a given time:

  • Valid: Everything it's ok and the user can see the vault ciphers with TOTP
  • Need Login: The user needs to log in using the iPhone
  • Need Setup: The user needs to set up an account with "Connect to Watch" enabled on their iPhone
  • Need Premium: The current account is not a premium account
  • Need 2FA item: The current account doesn't have any cipher with TOTP set up
  • Syncing: Displayed when changing accounts and syncing the new vault TOTPs
  • Need Device Owner Auth: The user needs to set up an Apple Watch Passcode in order to use the app

Persistence and encryption

On the Watch CoreData is used as persistence for the ciphers. So in order to encrypt the data in them a Value Transformer in each encrypted attribute is used: StringEncryptionTransformer.

Inside the transformer a call to the CryptoService is used that ends up using AES.GCM to encrypt the data with a 256 bits SymmetricKey. The key is generated/loaded the first time something needs to be encrypted and stored in the device Keychain.

Crash reporting

On all the other mobile applications, AppCenter is being used as Crash reporting tool. However, it doesn't have support for watchOS (nor its internal library to handle crashes).

So, on the watchOS app Firebase Crashlytics is used with basic crash reporting enabled (there is no handled error logging here yet). For this to work a GoogleService-Info.plist file is needed which is injected on the CI.

At the moment of writing this document, no plist is configured for dev environment so Crashlytics is enabled on non-DEBUG configurations.

There is a Log class to log errors happened in the app, but it's only enabled in DEBUG configuration.