1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-05 23:53:33 +00:00

Compare commits

..

144 Commits

Author SHA1 Message Date
larena1
95f5a1ce09 Remove 2023-03-06 20:29:28 +01:00
larena1
c4790eb78e Add TextVariationEmailAddress 2023-02-20 17:25:36 +01:00
larena1
8577765668 Finally stop filling password into username field
The logic in #2331 is unfortunately not very reliable as it'll only detect fields that have one of "email", "phone" or "username" in their id as username fields.
This commit ensures that additonally fields that have TextVariationWebEmailAddress are also detected as username fields.
2023-02-14 17:49:32 +01:00
Michał Chęciński
f63918aa4e Add apk hash in files to build and release (#2253)
* Add calculating hashes for apk files

* Install checksum

* Fix name of artifact

* Fix

* Add SHA files to github release

* Update .github/workflows/build.yml

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>

* Update .github/workflows/build.yml

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>

* Update .github/workflows/release.yml

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>

---------

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>
2023-02-14 10:10:29 -05:00
Carlos Gonçalves
f42c677d5a [SG-1056] Fix login prompt loop for added accounts (#2357)
* SG-1056 Fix login prompt loop for added accounts

* SG-1056 Fix PR comment
2023-02-10 17:49:05 +00:00
github-actions[bot]
5a56d64211 Autosync the updated translations (#2355)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-02-10 07:23:27 +01:00
André Bispo
0bd1b3f45f [SG-1052] Fix copy (#2353) 2023-02-09 15:13:50 -05:00
André Bispo
3780587991 [SG-1055] Init strength bar color (#2352) 2023-02-09 15:12:39 -05:00
André Bispo
6875389948 [SG-1057] Make check breaches true by default (#2351) 2023-02-09 15:09:52 -05:00
aj-rosado
0e5d6e79c5 [PS-1809] Updating the account premium state when syncing the vault (#2290)
* [PS-1809] Updating the account premium state when syncing the vault

* [PS-1809] Added validation to check if HasPremiumPersonally needs to be updated

* PS-1809 Renamed SetPremiumAsync to SetPersonalPremiumAsync
2023-02-08 12:03:02 +00:00
github-actions[bot]
490b74dd26 Autosync the updated translations (#2346)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-02-06 13:36:22 +01:00
github-actions[bot]
66d05e1b00 Autosync the updated translations (#2345)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-02-06 11:57:22 +01:00
André Bispo
3d3101c3ab [SG-1035] Fix label for log in with device (#2338) 2023-02-03 19:19:48 +00:00
github-actions[bot]
ccde4270d0 Autosync the updated translations (#2342)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-02-03 14:18:33 +01:00
github-actions[bot]
a1f799302e Autosync the updated translations (#2340)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-02-03 14:08:03 +01:00
André Bispo
4619e257e8 [SG-995] Remove automatic navigation from Home to Login if remembered (#2335) 2023-02-03 09:53:05 +00:00
Kyle Spearrin
c54a14cd3f set all kdf params on account profile (#2339) 2023-02-02 18:00:29 -05:00
aj-rosado
a96d95c95b [PS-2330][PS-2339][PS-2332][PS-2333] Checking for username terms when adding a password, if they exist don't add. Adding fields that contain username terms to the usernamefields collection (#2331) 2023-02-01 17:41:14 +00:00
André Bispo
e61ca489ce [SG-834] Mobile pending login requests management screen (#2281)
* Bootstrap new classes for settings list

* [SG-834] Add new method GetActivePasswordlessLoginRequestsAsync to AuthService

* [SG-834] Add generic handle exception method to BaseViewModel

* [SG-834] Add request verification to settings entry

* [SG-834] Add text resources

* [SG-834] Update view and viewmodel

* [SG-834] Remove unnecessary property assignment

* [SG-834] removed logger resolve
2023-02-01 12:22:17 +00:00
Bernd Schoolmann
c3ad5f0580 [PS-2358] Add kdf configuration options (#2328)
* Implement kdf configuration

* Remove unused import

* Move kdf parameters to kdfConfiguration struct

* Remove unused state migration service keys

* Revert newline changes in PCLCryptoFunctionService

* Update KdfConfiguration.cs

* Add checks for argon2, clean statemigration service

* Update constants

* Clean up code

* Further cleanup

* Change KdfType to non-nullable in SetKeyConnectorKeyRequest

---------

Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
2023-01-30 11:34:50 -05:00
Matt Bishop
8b08f906bd Remove fallout from test run (#2332) 2023-01-27 11:27:25 -05:00
github-actions[bot]
4ad5f5ae37 Autosync the updated translations (#2330)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-01-27 01:40:53 +01:00
André Bispo
68a6449339 [SG-516] Additional forwarded email providers for username generator - mobile (#2304)
* [SG-516] Added DuckDuckGo provider

* [SG-516] Add Fastmail as generator provider

* [SG-516] code clean up

* [SG-516] Default to service empty if first time on screen. Order services by alphabetic order.

* [SG-516] Removed unnecessary prop.

* [PS-2278] Fixed inverted eye bug.

* [SG-516] Add icon glyph converter

* [SG-516] Fixed enum default value and ordering
2023-01-26 13:53:48 +00:00
Kyle Spearrin
b8d53b0f81 Argon2id KDF (#2309)
* add gcc_flags for libargon2.a

* fix up ios proj

* remove unused tag

* add gcc_flags to ios projects

* ios libargon2 binary

* fix paths in ios project

* update pathing on other projs

* Argon2id primitive

* fix typing issues

* comment

* remove ds store

* [PS-2249] Implement Argon2 (#2293)

* Implement Argon2

* Fix incorrect argon2 type on iOS

* Switch argon2 implementation to native bindings

* Change argon2 to save iterations instead of memory as 'kdfIterations'

* Remove mistakenly added import

* Remove unused library

* cleanup

* move android libs

* move android libs

* Revert "move android libs"

This reverts commit 0b91b22cd2.

* Revert "move android libs"

This reverts commit 139839c469.

* PR feedback

Co-authored-by: Bernd Schoolmann <mail@quexten.com>
2023-01-25 07:58:36 -05:00
aj-rosado
dbfd15b819 [PS-2275] Update the default kdf iterations to 600k. (#2305)
* [PS-2275] Update the default kdf iterations to 350k.

* update to 600k per latest owasp
2023-01-24 20:08:52 +00:00
Daniel James Smith
acd0cb119d [PS-2324] Add honorific title mx (#2313) 2023-01-23 16:28:04 +01:00
André Bispo
d61bc4b5c1 [SG-460] Master Password security checks (mobile) (#2312)
* [SG-886] MasterPassword Strength Indicator (#2238)

* [SG-886] Add password strength indicator control

* [SG-570] Add weak password dialog check

* [SG-886] rename enum password strength

* [SG-886] Change control scale

* [SG-886] Move calculate user inputs to IPasswordGenerationService, refactor.

* [SG-886] Move formatted string to xaml. Move minimum chars to constant

* [SG-886] String to enum converter

* [SG-886] PR fixes. Code refactor control

* [SG-886] Update UI on OS theme change.

* [SG-886] Move colors to view

* [SG-886] Fixed password strength validation

* [SG-564][SG-565] Check Exposed Password (#2239)

* [SG-886] Add password strength indicator control

* [SG-570] Add weak password dialog check

* [SG-886] rename enum password strength

* [SG-564] [SG-565] Add check for exposed password and show dialog

* code format

* [SG-886] Change control scale

* [SG-886] Move calculate user inputs to IPasswordGenerationService, refactor.

* [SG-886] Move formatted string to xaml. Move minimum chars to constant

* [SG-886] String to enum converter

* [SG-886] Remove import

* [SG-886] Update UI on OS theme change.

* [SG-886] Move colors to view

* [SG-886] Fixed password strength validation
2023-01-20 13:38:31 +00:00
github-actions[bot]
5aa1146657 Autosync the updated translations (#2310)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-01-20 12:54:47 +01:00
André Bispo
f15fd246a8 Passwordless login 2FA not working (#2289)
* [SG-174] Fix 2FA passwordless login

* [SG-982] Enable login with device
2023-01-16 18:25:44 +00:00
Michał Chęciński
2b8547878a Fix autobump workflow (#2298) 2023-01-16 18:45:23 +01:00
github-actions[bot]
942d5d29d2 Bumped version to 2023.1.1 (#2297)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-01-16 10:29:15 +01:00
github-actions[bot]
fde63a836d Autosync the updated translations (#2292)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-01-13 10:03:24 +01:00
Brandon Maharaj
6102a0c115 [SG-912] Modify the mobile app to retrieve the user's avatar color (#2284)
* [SG-912] Modify the mobile app to retrieve the user's avatar color (#2277)

* work: baseline

* fix: dont use profile for store

* fiix: use userid in key

* fix: lookup on AccountView list create

* fix my own bad advice + tweaks

* Autosync the updated translations (#2279)

* fix my own bad advice + tweaks

* fiix: use userid in key

* [PS-1352] Fix ignore diacritics in search (#2044)

* Fix ignore diacritics in search

This change updates the search function to ignore diacritical marks in search results. Marks are stripped from both the search input and results.

* Removed logs, added null or whitespace validation and improved formatting


* [PS-2145] add rainsee browser series support (#2272)

* fix: lookup on AccountView list create

* Autosync the updated translations (#2279)

* fix my own bad advice + tweaks

* fix: single state grab is cool
2023-01-12 13:27:10 -05:00
Federico Maccaroni
4f4953206e [EC-469] Improve ApiException message (#2288)
* EC-469 Improve ApiException message to have the validation errors and message provided by the ErrorResponse

* EC-469 Updated default message format for ErrorResponse GetFullMessage()
2023-01-12 10:31:27 -03:00
github-actions[bot]
f772ee7068 Bumped version to 2023.1.0 (#2287)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-01-11 09:55:57 +01:00
Matt Bishop
8f93e6bf5f Upload and process test results as an artifact and report (#2286) 2023-01-10 13:48:22 -05:00
André Bispo
64fefac194 [SG-978] Added queries to http and https into manifest (#2282) 2023-01-06 13:46:03 -05:00
github-actions[bot]
d784b1290b Autosync the updated translations (#2279)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-01-06 01:23:41 +01:00
Auj625197595
0f2bc2fa25 [PS-2145] add rainsee browser series support (#2272) 2023-01-05 17:23:09 +00:00
noncenz
0e856d2add [PS-1352] Fix ignore diacritics in search (#2044)
* Fix ignore diacritics in search

This change updates the search function to ignore diacritical marks in search results. Marks are stripped from both the search input and results.

* Removed logs, added null or whitespace validation and improved formatting

Co-authored-by: aj-rosado <109146700+aj-rosado@users.noreply.github.com>
Co-authored-by: Andre Rosado <arosado@bitwarden.com>
2023-01-05 16:34:06 +00:00
mp-bw
acc587ce45 Fix repo url for F-Droid 1.15+ (#2276) 2023-01-04 14:28:47 -03:00
Daniel James Smith
66180397d9 Add needs-qa label to BANNED_LABELS (#2274) 2023-01-03 20:09:13 +01:00
Federico Maccaroni
571c4b8d22 EC-819 Update iOS app icon (#2270) 2022-12-30 17:52:25 +00:00
Federico Maccaroni
660ba3d722 [EC-628] Added Crashlytics to the watchOS project (#2267)
* EC-628 Added Crashlytics to the watchOS project, missing GoogleService-Info.plist for now

* addition of GoogleServices-info.plist.gpg

* Re-add GoogleService-info.plist.gpg

* EC-628 Updated build.yml to decrypt and copy the GoogleService-Info.plist for Crashlytics and also added step to upload Watch dSYMs to Crashlytics

* EC-628 Fix run command upload watch dsym build.yml

* EC-628 Updated Apple iOS macos version to 12 in order to resolve Watch XCode dependencies correctly with SPM

* EC-628 Added some logs to see where I'm located cause it's issuing a wrong path. Also deactivated droid builds so that it's faster to test and doesn't consume resources on that till the build is OK

* EC-628 fixed variable reference build.yml

* EC-628 Removed ls of watch dsym export path before creating the folder that was causing the build to fail

* EC-628 Removed XCode build phase step to auto-upload dsym to firebase and added some logs to find the upload-symbols tool from SPM. Also fix making the dir for the watchOS dSYMs export path

* EC-628 Changed approach to upload watch dSYMs to Firebase by finding the upload-symbols script dynamically with find and then executing it

* EC-628 Added missing ; to command to upload watch dSYMs

* EC-628 Fix buld.yml Watch dSYMs copy to export path

* EC-628 Cleaned build.yml for the watch dSYMs upload to Firebase and bring back droid builds

Co-authored-by: sneakernuts <671942+sneakernuts@users.noreply.github.com>
2022-12-30 17:51:35 +00:00
Kevinlinpr
414cb9bd7e [PS-2116] feat(Support): Add Lemur Browser (#2262)
*  feat(Support): Add Lemur Browser

Lemur Browser is the mobile browser support Chrome and Edge extensions, build from chromium.

🔗 issue - https://github.com/bitwarden/mobile/issues/2260
🔗 Lemur Browser - https://www.lemurbrowser.com/

* 🐞 fix(Sorted alphabetically): Move up one line

Please move this up one line to ensure the sections are sorted alphabetically

6d5cb73445
2022-12-30 13:38:05 +01:00
github-actions[bot]
e588efd0a1 Autosync the updated translations (#2271)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2022-12-30 01:19:00 +01:00
Federico Maccaroni
1cdba5f73d EC-868 clear console logging (#2265) 2022-12-29 15:00:13 -03:00
Dan Alcantara
cbccd10271 Add support for the Neeva app (#2045) 2022-12-28 13:19:16 +01:00
Şahin BEZENG
6de6b19944 Add support for Dezor Browser (#2142) 2022-12-28 13:08:40 +01:00
Daniel James Smith
5493b00957 Fix link to contributing docs (#2266) 2022-12-26 17:38:55 +01:00
Federico Maccaroni
81988a7fe7 EC-820 Added icons for Watch Complication and updated the Watch app icons (#2258) 2022-12-23 22:39:50 +00:00
André Bispo
b91ff09e27 [SG-174] Remove login with device button (#2259) 2022-12-23 17:11:14 +00:00
github-actions[bot]
945627d649 Autosync the updated translations (#2257)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2022-12-23 01:53:00 +01:00
Federico Maccaroni
307c71ee07 EC-847 improved watch list ui performance by removing the animations on the search box and going back to the simple List instead of the TrackableWithHeaderListView (#2256) 2022-12-22 19:06:21 +00:00
Federico Maccaroni
e72932cbaa [EC-835] Add kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly on Watch Keychain (#2250)
* EC-835 Added in the Watch app keychain accessible when passcode set this device only and when the passcode is set to signal the iPhone to trigger a sync on opening the watch app

* EC-835 Embed LocalAuthentication framework into the watch app to fix no such module when importing it

* EC-835 Changed approach to check if Watch has passcode enabled by using Keychain accessible kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly instead of LAContext

* EC-835 Fix weird error saying unassigned local variable on the CI compiler. It seems it doesn't realize of the full condition
2022-12-22 17:59:12 +00:00
mp-bw
4347c2f81d Android: Show auto-totp-copy toast on pre-13 devices (#2249) 2022-12-19 14:03:22 -03:00
github-actions[bot]
619acfe0fe Autosync the updated translations (#2247)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2022-12-16 18:30:52 +01:00
André Bispo
05765c2af9 Remove unused resource entry SelectAddTotpToStoreTheKeySafely (#2248) 2022-12-16 16:26:59 +01:00
Federico Maccaroni
728182de6c [EC-844] Improve Apple Watch states (#2246)
* EC-844 improve need login / need setup states on the watch

* EC-844 Fix naming of things and moved constant to the proper place to maintain format. Also removed UpdateLastUserShouldConnectToWatchAsync from the interface of the StateService given that it's not used outside the service and made it private
2022-12-15 15:21:29 -03:00
Federico Maccaroni
a19b5c4e05 EC-842 Delete items that are not included in the sync to the watch (#2242) 2022-12-15 13:03:34 -03:00
Federico Maccaroni
f6895a0733 EC-846 Revert build.yml Android was not being built because a condition added while developing the Apple Watch app in another branch merged to master (#2245) 2022-12-15 10:57:10 -03:00
Federico Maccaroni
3e48b7a968 EC-785 Bumped Apple Watch version to 2022.12.0 (#2243) 2022-12-14 22:03:06 +00:00
Todd Martin
ebf65ecb96 Set push token state values to be user-specific (#2200)
* Changed the current push token and last registration time to be user-based

* Fixed compile error.

* Fixed interface implementation.

* Fixed compile error for Android.

* Refactored to handle getting active user ID within state service

* Refactored methods allow existing logic to handle getting the active user.

* Updated to reconcile options.

* Updated naming and fixed issue with UserId.

* Removed space between constants.
2022-12-14 16:07:04 -05:00
github-actions[bot]
37dab0928b Bumped version to 2022.12.0 (#2241)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2022-12-14 21:04:08 +00:00
André Bispo
aa0544cd3d [SG-872] Create generic exception handler for mobile (#2222)
* [SG-872] Add method to handle exception in BaseViewModel

* [SG-872] Code format
2022-12-13 21:53:04 +00:00
André Bispo
f4b4cfc9de [SG-870] Add null check on notification received (#2221) 2022-12-13 21:50:05 +00:00
Federico Maccaroni
7785b2dbf9 EC-841 Fix Watch fetch cipher and added condition for previews (#2236) 2022-12-13 19:36:45 +00:00
Federico Maccaroni
e3dafb502b EC-834 Watch UI/UX fixes (#2234) 2022-12-13 14:50:55 -03:00
Federico Maccaroni
b30fc12135 EC-830 Fix state checking when watch app had already been installed before, so reset it if first run. (#2232) 2022-12-13 11:27:33 -03:00
Federico Maccaroni
28d204f2b1 EC-833 fix WatchDeviceService registration to be called from the extensions appropiately (#2233) 2022-12-13 11:27:15 -03:00
github-actions[bot]
1a3ff5ec41 Autosync the updated translations (#2230)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2022-12-09 11:59:54 +01:00
flawedworld
2f6fd476a2 Add support for Vanadium in GrapheneOS (#2199)
Co-authored-by: mp-bw <59324545+mp-bw@users.noreply.github.com>
2022-12-08 10:07:06 -05:00
mp-bw
7a08452fa8 Automatically continue when enter key pressed during email entry (#2227) 2022-12-07 16:51:32 +00:00
Álison Fernandes
fa6bac3b43 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>
2022-12-07 11:39:20 -05:00
André Bispo
2a60ff62d8 [SG-601] Enhance experience when user denies camera access in Authenticator (#2205)
* [SG-601] Handle camera denied permissions

* [SG-601] Code format

* [SG-601] PR Fixes

* [SG-601] Change resource text to singular

* [SG-601] Remove horizontal and vertical options

* [SG-601] Add start and stop scanner methods

* [SG-601] Remove parameter from ScanPage

* [SG-601] Move initialization to viewmodel

* [SG-601] Fix zxing scanning bug

* [SG-601] Move RunUIThread inside of method
2022-12-06 16:30:46 +00:00
github-actions[bot]
eaa4f193ce Bumped version to 2022.11.1 (#2224)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2022-12-05 13:56:00 -08:00
mp-bw
6973a0b71c [BEEEP] Support for automatic TOTP token copy via external autofill (Android) (#2220)
* Android: Support for automatic TOTP copy via external autofill

* update iOS autofill interface

* additional tweaks
2022-12-05 12:49:34 -05:00
Opeyemi
bafd9ff85d update ::set-output to the latest method (#2217)
* update ::set-output to the latest method

* Update workflow for linter error
2022-12-05 15:04:34 +00:00
aj-rosado
4580033477 [PS-1707] improve dark layout colors (#2209)
* [PS-1707] Changed the callout color to a Theme Color

* [PS-1707] Changed color defaults to defined colors

* [PS-1707] Added an effect to change the switch hint color to a theme color

* [PS-1707] Removed HintColorSwitchEffect and added the content to the CustomSwitchRenderer to affect all switches and update on theme change
2022-12-02 16:38:25 +00:00
github-actions[bot]
a4673021a9 Autosync the updated translations (#2215)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2022-12-02 01:28:13 +01:00
github-actions[bot]
8e2e143e6f Bumped version to 2022.11.0 (#2212)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2022-12-01 16:56:36 +00:00
André Bispo
f43f9171e5 [SG-860] Add condition to execute notification tap code (#2211) 2022-12-01 11:58:27 +00:00
github-actions[bot]
3c8a8862ca Autosync the updated translations (#2204)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2022-11-25 01:26:09 +01:00
Álison Fernandes
969b89bb4d SG-837 Fixes: QA build crashing while attaching (#2203) 2022-11-24 15:49:12 +00:00
github-actions[bot]
164b8970d3 Autosync the updated translations (#2198)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2022-11-18 04:39:37 +01:00
André Bispo
34fd30e157 [SG-831] Pull Down Sync does not retrieve pending AuthRequests (#2196)
* [SG-831] Pull to refresh forces refresh.

* [SG-831] Expose sync login request method to be used independently

* [SG-831] Change sync order
2022-11-17 16:02:34 +00:00
André Bispo
8e09f0cc15 [SG-808] Change navigation on remove account after logout by timeout (#2193) 2022-11-16 12:38:45 +00:00
André Bispo
693a4ef776 [SG-816] Get all login requests and pick the most recent (#2191)
* [SG-816] Get all login requests anfd pick the most recent

* [SG-816] Add check if active user has approve login with device active

* [SG-816] Build fix. Fix response model.

* [SG-816] Move code to sync service
2022-11-15 17:36:21 +00:00
André Bispo
0992a989d4 [SG-778] Adjust mobile client to handle previously-responded-to passwordless request (#2190)
* [SG-778] Add properties to response model

* [SG-778] Add validation for request already answered

* [SG-778] Remove unnecessary properties

* [SG-778] Remove unnecessary assignments
2022-11-15 14:17:26 +00:00
Todd Martin
1b137a8a8a Added check to make sure it doesn't set a deployment on a Dry Run. (#2188) 2022-11-14 13:43:45 -05:00
André Bispo
7e8e86a77a [SG-813] Not You? crashes app after vault logout timeout (#2184)
* Merge branch 'master' into feature/SG-174-login-with-device

* [SG-813] Fix merge

* [SG-813] rename HomePage parameter name

* [SG-813] Added NavParams for home page on account switching.

* [SG-813] Remove account showing when adding new account.

* [SG-813] Add account switch pop up if email already exists

* [SG-813] Add default account avatar to HomePage

* [SG-813] Code format

* [SG-813] Remove unused import

* [SG-813] Renamed checkNavigateLogin to shouldCheckRememberEmail

* [SG-813] Move prompt account switch to account manager service

* [SG-813] Remove Account button appears if email is the same as the ActiveUser

* [SG-813] Fix code duplicate

* [SG-813] Fix for android RemoveAccount button

* [SG-813] Code format
2022-11-11 18:14:30 +00:00
Robyn MacCallum
20c1e2d7f2 Add syncCompleted to check to get passwordless requests (#2186) 2022-11-11 12:33:17 -05:00
github-actions[bot]
7870c2706b Autosync the updated translations (#2185)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2022-11-11 12:34:15 +01:00
Patrick
5a5f50605b Update AppResources.resx (#2181) 2022-11-10 13:44:18 -05:00
Robyn MacCallum
0106732cbd [SG-808] Only show 'remove account' option if user is in Accounts list (#2179)
* Only show 'remove account' option if user is in Accounts list

* Condense existing email logic

* Add line break

* Condense to local variable

* Cleanup spacing
2022-11-10 07:06:17 -05:00
André Bispo
9ae269dd57 [SG-174] Login with Device Request - Mobile (#2167)
* [SG-174] Add new login request services to Api

* [SG-174] Fix typo

* [SG-174] Enable login with device button.

* [SG-174] Add new login request page and viewmodel

* [SG-174] Add new text resources

* [SG-174] Add new RSA Decrypt method with string param

* [SG-174] Change create login request method

* [SG-174] Add new method to auth service to login passwordless

* [SG-174] Refactor login helper method to work with passwordless

* [SG-174] Fix service registration

* [SG-174] Update token request to support passwordless

* [SG-174] Update Api service with passwordless methods

* [SG-174] Fix App csproj references

* [SG-174] Remove unnecessary argument

* [SG-174] dotnet format

* [SG-174] Fixed iOS Extensions

* [SG-174] Change Command to ICommand

* [SG-174] Change Gesture Recognizer to Command

* [SG-174] Fix close action

* [SG-174] Code format

* [SG-174] Fix android frame shadow bug

* [SG-174] PR fixes
2022-11-09 16:25:48 +00:00
Opeyemi
04ed47d545 Devops 1039 update release flow dry run step names (#2177) 2022-11-08 22:44:50 +00:00
Opeyemi
6160535c03 add run-name for release to include workflow trigger (#2174) 2022-11-08 20:27:22 +00:00
André Bispo
8d92373c88 [SG-806] add try catch on initAsync (#2173) 2022-11-07 13:41:02 +00:00
André Bispo
6bfc8f7d49 [SG-802] Add if check on notification tap. (#2172) 2022-11-07 13:40:48 +00:00
André Bispo
69f02eef82 [SG-800] Selecting notification for inactive account on iOS while app is open does not display the request (#2166)
* [SG-800] Remove await from ActionSheet
If the user never answers the pop up, the lock is never released.

* [SG-800] Add try catch
2022-11-04 16:54:16 +00:00
github-actions[bot]
02b8e54988 Autosync the updated translations (#2165)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2022-11-04 08:11:43 +01:00
Álison Fernandes
99472d2548 [EC-655] Adds build variants to the mobile codebase using a CAKE script (#2161)
* Implemented CAKE build script

* cake script now deals with all of iOS's .plists

* cake now updates iOS bundleid's / Android packagename in codefiles

* iOS Bundle ID / Name should be correctly handled now + refactor

* tabs -> spaces

* Additional code files are now handled by cake

* Additional iOS codefile changes required

* Android's Autofill Label is now changed

* Removed dash from packagenames / bundleIDs

* Fixed CFBundleURLName set

* Added google-services.json to cake preprocessing

* Add CAKE to build workflow
- Android

* Add debug

* Updated cake's GitVersion.Tool

* AndroidManifest manual parsing needs to happen first

* Added Android Constants to build.cake

* [SG-747] Add Android QA build to mobile build pipeline (#2144)

* Add checkout depth

* Build and upload QA artifacts

* Remove missing .aab

* Update build.yml

* Update paths

* Update var names

* Build and upload QA artifacts

* Add in matrix to path.

* Lets not fail all the jobs if something pukes

* Add in some flow logic for QA

* We need strings in pwsh

* Remove extra quotes

* Testing, remove uneeded runs

* Test folder items

* [SG-747] Added more debug info to find problem

* [SG-747] copy signed apk to correct file name for each app variant

* [SG-747] try to fix if statement

* [SG-747] separate decrypt google services into another step with condition.

* [SG-747] fixed typo and line break

* [SG-747] added debug to check output path

* [SG-747] fix package name

* [SG-747] Fixed condition of step execution

* [SG-747] test if cases

* [SG-747] Code clean up

* [SG-747] Added FDroid and iOS steps.

* [SG-747] Removed test step

* [SG-747] Step name changes

* Update condition to be more inclusive

Co-authored-by: Álison Fernandes <vvolkgang@users.noreply.github.com>

* [SG-747] Expand if condition to allow more build types other than QA

* [SG-747] removed execution condition

Co-authored-by: mimartin12 <77340197+mimartin12@users.noreply.github.com>

* Apply suggestions from code review

Linter suggestions

Co-authored-by: mimartin12 <77340197+mimartin12@users.noreply.github.com>
Co-authored-by: Micaiah Martin <mmartin@bitwarden.com>
Co-authored-by: mimartin12 <77340197+mimartin12@users.noreply.github.com>
Co-authored-by: Álison Fernandes <vvolkgang@users.noreply.github.com>
Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com>

* Base bundle ID refactor and cleaned up TODOs

- Added base vars for the bundle IDs
- Removed a TODO and explained the remaining ones
- Commented a unused var, keeping it in the code as this might be useful later

Co-authored-by: Micaiah Martin <mmartin@bitwarden.com>
Co-authored-by: Federico Andrés Maccaroni <fedemkr@gmail.com>
Co-authored-by: Todd Martin <tmartin@bitwarden.com>
Co-authored-by: André Bispo <abispo@bitwarden.com>
Co-authored-by: mimartin12 <77340197+mimartin12@users.noreply.github.com>
Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com>
2022-11-01 22:14:49 +00:00
Carlos Gonçalves
5a4c81bd75 SG-769 - Ensured token for decoding is refreshed before use (#2150) 2022-10-31 15:46:10 -04:00
Carlos Gonçalves
ee09c0abda SG-786 - Fix 400 error code log outs without invalid_grant (#2156)
* SG-786 - Added validation to check if the 400 error is invalid grant

* SG 786 - Improved code quality
2022-10-31 13:40:26 -04:00
André Bispo
9baa79e10b [SG-773][SG-775] Duplicate passwordless login requests (#2160)
* [SG-773] Change method call to message send

* [SG-773] Introduce lock to avoid concurrent executions of login requests

* [SG-773][SG-775] add comment

* [SSG-773][SG-775] Refactor passwordlessLoginRequest string to constant
2022-10-31 15:57:09 +00:00
André Bispo
a8909a3ce6 [SG-765] Add cancel button to passwordless login modal on android (#2159) 2022-10-31 15:06:37 +00:00
André Bispo
b9b9c2e5ff [SG-166] Two Step Login - Feature Branch (#2157)
* [SG-166] Update fonts to have necessary icons

* [SG-166] Add new custom view to hold a button with a font icon and a label.

* [SG-166] Two Step login flow - Mobile (#2153)

* [SG-166] Add UI elements to Home and Login pages. Change VMs to function with new UI. Add new string resources.

* [SG-166] Pass email parameter from Home to Login page.

* [SG-166] Pass email to password hint page.

* [SG-166] Remove remembered email from account switching.

* [SG-166] Add GetKnownDevice endpoint to ApiService

* [SG-166] Fix GetKnownDevice string uri

* [SG-166] Add Renderer for IconLabel control. Add RemoveFontPadding bool property.

* [SG-166] include IconLabelRenderer in Android csproj file

* [SG-166] Add new control. Add styles for the control.

* [SG-166] Add verification to start login if email is remembered

* [SG-166] Pass default email to hint page

* [SG-166] Login with device button only shows if it is a known device.

* [SG-166] Change Remember Email to Remember me. Change Check to Switch control.

* [SG-166] Add command to button for SSO Login

* Revert "[SG-166] Update fonts to have necessary icons"

This reverts commit 472b541cef.

* [SG-166] Remove IconLabel Android renderer. Add RemoveFontPadding effect.

* [SG-166] Update font with new device and suitcase icon

* [SG-166] Fix RemoveFontPadding effect

* [SG-166] Remove unused property in IconLabel

* [SG-166] Fix formatting on IconLabelButton.xaml

* [SG-166] Update padding effect to IconLabel

* [SG-166] Add control variable to run code once on create

* [SG-166] Add email validation before continue

* [SG-166] Refactor icons

* [SG-166] Update iOS Extension font

* [SG-166] Remove HomePage login btn step

* [SG-166] Make clickable area smaller

* [SG-166] Fix hint filled by default

* [SG-166] Fix IconButton font issue

* [SG-166] Fix iOS extension

* [SG-166] Move style to Base instead of platforms

* [SG-166] Fix layout for IconLabelButton

* [SG-166] Switched EventHandler for Command

* [SG-166] Removed event handler

* [SG-166] Fix LoginPage layout options

* [SG-166] Fix extensions Login null email

* [SG-166] Move remembered email logic to viewmodel

* [SG-166] Protect method and show dialog in case of error

* [SG-166] Rename of GetKnownDevice api method

* [SG-166] rename text resource key name

* [SG-166] Add close button to iOS extension

* [SG-166] Switch event handlers for commands

* [SG-166] Change commands UI thread invocation.

* [SG-166] Remove Login with device button from the UI

* [SG-166] Fixed appOptions and close button on iOS Extensions
2022-10-28 23:10:41 +01:00
github-actions[bot]
89adab6784 Autosync the updated translations (#2155)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2022-10-28 02:40:47 +02:00
mp-bw
77d8afcfe6 added missing id check to obj comparison (#2154) 2022-10-27 12:09:33 -04:00
Michał Chęciński
4fcb063190 Move renovate.json to .github folder (#2152) 2022-10-27 15:49:29 +02:00
mp-bw
5deba15373 Updated avatar color selection logic (#2151)
* updated avatar color selection logic

* tweaks

* more tweaks

* formatting
2022-10-26 12:34:54 -04:00
Carlos Gonçalves
505426cd6a [SG 547] Mobile username generator iOS.Extension UI changes (#2140)
* [SG-547] - Added button to generate username when using iOS extension

* [SG-547] - Missing changes from last commit

* SG-547 - Added missing interface method

* SG-547 - Added token renovation for iOS.Extension flow

* SG-547 Replaced generate buttons for icons

* SG-547 Removed unnecessary validation

* SG-547 - Fixed PR comments

* SG 547 - Missing file from last commit

* SG-547 - Fixed PR comments

* SG-547 - Renamed method
2022-10-25 21:05:15 +01:00
github-actions[bot]
3cb9f37997 Autosync the updated translations (#2146)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2022-10-21 12:18:18 +02:00
aj-rosado
4c2f5c05e5 [PS-1188] Add new Solarized Dark Theme (#2141)
* PS-1188 Add new Solarized Dark Theme
2022-10-19 10:57:01 +01:00
André Bispo
eefc9bd239 [SG-705] Popup when a request for authentication comes in on a logged-in account that is not active (#2135)
* [SG-705] Added pop up to perform account switching if the user receives a login request from another account.

* [SG-705] missing resource designer

* [SG-705] Fixed wrong key for state service variable.

* [SG-705] Fix formatting of account switch alert.

* [SG-705] dotnet format run

* [SG-705] Removed async

* [SG-705] Refactor on App
2022-10-18 17:21:45 +01:00
sneakernuts
d18efdea73 updated version bump + crowdin-pull workflows (#2119)
* updated version bump + crowdin-pull workflows

* updated retrieve secrets step and changed gpg vars to reference kv secrets

* Removed duplicate step

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>
2022-10-17 15:19:06 +00:00
Michał Chęciński
a72a0ec0ce Fix autobump workflow (#2138) 2022-10-17 11:50:38 +02:00
André Bispo
c7e9f30a9a [SG-703] Login request is not removed after dismissing push notification (#2134)
* [SG-703] Added category to iOS notifications in order to be able to receive dismiss actions

* [SG-703] PR Fix
2022-10-14 17:35:17 +01:00
mp-bw
6c404c8229 Testing removal of Xamarin.Forms dependency from iOS.ShareExtension (#2137) 2022-10-14 15:53:18 +01:00
github-actions[bot]
e5de530c2c Autosync the updated translations (#2133)
Co-authored-by: github-actions <>
2022-10-14 14:00:38 +02:00
github-actions[bot]
3a87378847 Bumped version to 2022.10.1 (#2132)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-10-13 12:43:48 -07:00
Danielle Flinn
6048a10d6d [PS-1543]Sentence case mobile (#2095)
* transitioned to sentence case
* removed unnecessary punctuation
* other small content improvements
2022-10-13 12:31:03 -07:00
github-actions[bot]
a5ad43b134 Bumped version to 2022.10.0 (#2130)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-10-12 17:42:14 +01:00
André Bispo
539f8fe5b5 [SG-690] Timeout fix (#2129)
[SG-690] Updated server times to utc
2022-10-12 15:55:38 +01:00
André Bispo
569922805f [SG-703] Login request is not removed after dismissing push notification (#2125)
* [SG-703] Handle iOS dismiss notification action. Added core logic to remove passwordless notification from local storage.

* [SG-702] Added broadcast receiver to catch dismiss notfication events on android.

* [SG-703] PR fixes.

* [SG-703] Fix constants namespaces. Lazyloading services on broadcast receiver.

* [SG-703] Change services to use lazy loading

* [SG-703] Change lazy loading to be parameterless.
2022-10-12 15:55:01 +01:00
github-actions[bot]
3972e3de8a Autosync the updated translations (#2118)
Co-authored-by: github-actions <>
2022-10-12 15:14:24 +02:00
Federico Maccaroni
ba677a96aa [EC-519] Refactor Split DeviceActionService (#2081)
* EC-519 Refactored IDeviceActionService to be split into IFileService and IAutofillManager also some cleanups were made

* EC-519 Fix format

* EC-519 Fix merge to use the new AutofillHandler
2022-10-11 18:19:32 -03:00
Federico Maccaroni
d800e9a43e EC-602 Constants namespace fix (#2127) 2022-10-11 16:47:52 -04:00
Federico Maccaroni
2d35a00caa EC-602 added droid constants file with the package name constant (#2126) 2022-10-11 16:08:36 -04:00
Michał Chęciński
dc5698b353 [DEVOPS-1014] Fix version auto bump workflow (#2121)
* Fix autobump

* Fix regex

* Use tag name

* DEVOPS-1014 - Review (#2123)

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>
2022-10-11 16:27:07 +02:00
André Bispo
abada481b7 [SG-702] Tapping Push Notification does not open the account the request is for (#2112)
* [SG-702] Tap notification now switches accounts if it is a passwordless notification.

* [SG-702] Fix compilation errors

* [SG-702] Fixed iOS notification tap fix

* [SG-702] Notification data model

* [SG-702] Change method signature with object containing properties. PR fixes.
2022-10-07 12:06:57 +01:00
André Bispo
1e5eab0574 [SG-690] DateTime to Utc fix (#2115)
* [SG-690] DateTime to Utc fix

* [SG-690] Removed Utc from server side datetime.
2022-10-04 20:40:28 -04:00
André Bispo
c1101af582 [SG-687] added try catch to cancellation token disposal. (#2114) 2022-10-04 20:25:52 +01:00
André Bispo
1db4c4fc8b SG-687 Request time not updating (#2108)
* [SG-687] request updates text with timer task and expires after 15 mins.

* [SG-697] PR Fixes

* [SG-687] Ran code format

* [SG-687] PR Fixes

* [SG-687] missed constant replacement

* [SG-687] PR fixes
2022-10-03 17:42:33 +01:00
André Bispo
bc949fe87a [SG-691] Login request is not displayed after changing accounts (#2111)
* [SG-691] Added new message to be broadcasted when account is switched to trigger a check for login requests.

* [SG-691] PR fixes
2022-10-03 17:11:38 +01:00
Carlos Gonçalves
a890ee6612 [SG-666][SG-667] Email is not prefilled and username isn't generated automatically (#2109)
* SG-666 SG-667 - Email is now prefilled for plus addressed email username type
* Username is auto generated upon navigation

* SG-666 - Fixed PR comments
* Added missing property initialization
2022-10-03 16:51:22 +01:00
André Bispo
90e0b5dcf0 [SG-690] Login Request does not disappear after 15 minutes (#2106)
* [SG-690] Add timeout of 15 for android notifications. Add condition to not prompt login requests if 15mins have passed. Add constant for timeout time.

* [SG-690] Added dialog on click confirm/deny if the request is expired.

* [SG-690] PR fixes

* [SG-690] PR fixes
2022-09-30 20:44:56 +01:00
mp-bw
9631988fc2 remove duplicate/older community toolkit dependency (#2107) 2022-09-30 15:44:18 -03:00
André Bispo
b5f80da28d [SG-696] Android notification icon blank (#2105)
* [SG-696] Added new icon for android notifications. Changed notification intent flags to use helper.

* [SG-696] PR fix. Min version was already 21.
2022-09-30 14:47:03 +01:00
github-actions[bot]
7e0b943b70 Autosync the updated translations (#2104)
Co-authored-by: github-actions <>
2022-09-30 13:10:59 +02:00
mp-bw
425be32c15 added a11y disclosure prompt for Android (#2102) 2022-09-27 15:03:06 -03:00
André Bispo
f9a32e4abc Passwordless feature branch PR (#2100)
* [SG-471] Passwordless device login screen (#2017)

* [SSG-471] Added UI for the device login request response.

* [SG-471] Added text resources and arguments to Page.

* [SG-471] Added properties to speed up page bindings

* [SG-471] Added mock services. Added Accept/reject command binding, navigation and toast messages.

* [SG-471] fixed code styling with dotnet-format

* [SG-471] Fixed back button placement. PR fixes.

* [SG-471] Added new Origin parameter to the page.

* [SG-471] PR Fixes

* [SG-471] PR fixes

* [SG-471] PR Fix: added FireAndForget.

* [SG-471] Moved fire and forget to run on ui thread task.

* [SG-381] Passwordless - Add setting to Mobile (#2037)

* [SG-381] Added settings option to approve passwordless login request. If user has notifications disabled, prompt to go to settings and enable them.

* [SG-381] Update settings pop up texts.

* [SG-381] Added new method to get notifications state on device settings. Added userId to property saved on device to differentiate value between users.

* [SG-381] Added text for the popup on selection.

* [SG-381] PR Fixes

* [SG-408] Implement passwordless api methods (#2055)

* [SG-408] Update notification model.

* [SG-408] removed duplicated resource

* [SG-408] Added implementation to Api Service of new passwordless methods.

* removed qa endpoints

* [SG-408] Changed auth methods implementation, added method call to viewmodel.

* [SG-408] ran code format

* [SG-408] PR fixes

* [SG-472] Add configuration for new notification type (#2056)

* [SG-472] Added methods to present local notification to the user. Configured new notification type for passwordless logins

* [SG-472] Updated code to new api service changes.

* [SG-472] ran dotnet format

* [SG-472] PR Fixes.

* [SG-472] PR Fixes

* [SG-169] End-to-end testing refactor. (#2073)

* [SG-169] Passwordless demo change requests (#2079)

* [SG-169] End-to-end testing refactor.

* [SG-169] Fixed labels. Changed color of Fingerprint phrase. Waited for app to be in foreground to launch passwordless modal to fix Android issues.

* [SG-169] Anchored buttons to the bottom of the screen.

* [SG-169] Changed device type from enum to string.

* [SG-169] PR fixes

* [SG-169] PR fixes

* [SG-169] Added comment on static variable
2022-09-26 18:27:57 +01:00
André Bispo
2f4cd36595 [SG-671] OTP Menu Screen causes Crash on Android (#2097)
* [SG-671] removed unnecessary calc of otpauth period. protected cal of otpauth from crashing the app if url has a wrong format.

* [SG-671] changed logger

* [SG-671] Refactored GetQueryParams code to used HttpUtility.ParseQueryString.

* [SG-671] refactor and null protection.

* [SG-671] code format

* [SG-671] fixed bug where totp circle countdown was fixed to 30.

* [SG-167] added fallback for uri check. Changed all default totp timers to constant.

* [SG-671] missed unsaved file

* [SG-671] simplified code
2022-09-26 17:51:03 +01:00
476 changed files with 35704 additions and 6717 deletions

View File

@@ -7,6 +7,12 @@
"commands": [
"dotnet-format"
]
},
"cake.tool": {
"version": "2.2.0",
"commands": [
"dotnet-cake"
]
}
}
}

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.

Binary file not shown.

View File

@@ -4,10 +4,10 @@ name: Build
on:
push:
branches-ignore:
- 'l10n_master'
- 'gh-pages'
- "l10n_master"
- "gh-pages"
paths-ignore:
- '.github/workflows/**'
- ".github/workflows/**"
workflow_dispatch:
inputs: {}
@@ -42,15 +42,15 @@ jobs:
id: branch-check
run: |
if [[ $(git ls-remote --heads origin rc) ]]; then
echo "::set-output name=rc_branch_exists::1"
echo "rc_branch_exists=1" >> $GITHUB_OUTPUT
else
echo "::set-output name=rc_branch_exists::0"
echo "rc_branch_exists=0" >> $GITHUB_OUTPUT
fi
if [[ $(git ls-remote --heads origin hotfix-rc) ]]; then
echo "::set-output name=hotfix_branch_exists::1"
echo "hotfix_branch_exists=1" >> $GITHUB_OUTPUT
else
echo "::set-output name=hotfix_branch_exists::0"
echo "hotfix_branch_exists=0" >> $GITHUB_OUTPUT
fi
shell: bash
@@ -59,15 +59,22 @@ jobs:
name: Android
runs-on: windows-2022
needs: setup
strategy:
fail-fast: false
matrix:
variant: ["prod", "qa"]
steps:
- name: Setup NuGet
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
with:
nuget-version: 5.9.0
- name: Set up MSBuild
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
- name: Setup Windows builder
run: choco install checksum --no-progress
- name: Work Around for broken Windows 2022 Runner Image
run: |
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
@@ -87,7 +94,6 @@ jobs:
Write-Host "components were not installed"
exit 1
}
- name: Print environment
run: |
nuget help | grep Version
@@ -98,7 +104,8 @@ jobs:
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
with:
fetch-depth: 0
- name: Decrypt secrets
env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
@@ -109,12 +116,17 @@ jobs:
--output ./src/Android/app_play-keystore.jks ./.github/secrets/app_play-keystore.jks.gpg
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output ./src/Android/app_upload-keystore.jks ./.github/secrets/app_upload-keystore.jks.gpg
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output ./src/Android/google-services.json ./.github/secrets/google-services.json.gpg
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output $HOME/secrets/play_creds.json ./.github/secrets/play_creds.json.gpg
shell: bash
- name: Decrypt secrets - Google Services
if: ${{ matrix.variant == 'prod' }}
env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
run: |
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output ./src/Android/google-services.json ./.github/secrets/google-services.json.gpg
shell: bash
- name: Increment version
run: |
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
@@ -139,29 +151,48 @@ jobs:
shell: pwsh
- name: Run Core tests
run: dotnet test test/Core.Test/Core.Test.csproj
run: dotnet test test/Core.Test/Core.Test.csproj --logger "trx;LogFileName=test-results.trx"
shell: pwsh
- name: Report test results
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226
if: always()
with:
name: Test Results
path: "**/test-results.trx"
reporter: dotnet-trx
fail-on-error: true
- name: Build Play Store publisher
if: ${{ matrix.variant == 'prod' }}
run: dotnet build ./store/google/Publisher/Publisher.csproj -p:Configuration=Release
- name: Build for Play Store
- name: Setup Android build (${{ matrix.variant }})
run: dotnet cake build.cake --target Android --variant ${{ matrix.variant }}
- name: Build Android
run: |
$configuration = "Release";
Write-Output "########################################"
Write-Output "##### Build $configuration Configuration"
Write-Output "########################################"
msbuild "$($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj")" "/p:Configuration=$configuration"
shell: pwsh
- name: Sign for Play Store
- name: Sign Android Build
env:
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
run: |
$androidPath = $($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj");
$packageName = "com.x8bit.bitwarden";
if ("${{ matrix.variant }}" -ne "prod")
{
$packageName = "com.x8bit.bitwarden.${{ matrix.variant }}";
}
Write-Output "########################################"
Write-Output "##### Sign Google Play Bundle Release Configuration"
Write-Output "########################################"
@@ -175,9 +206,8 @@ jobs:
Write-Output "##### Copy Google Play Bundle to project root"
Write-Output "########################################"
$signedAabPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/com.x8bit.bitwarden-Signed.aab");
$signedAabDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden.aab");
$signedAabPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/$($packageName)-Signed.aab");
$signedAabDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).aab");
Copy-Item $signedAabPath $signedAabDestPath
Write-Output "########################################"
@@ -193,33 +223,69 @@ jobs:
Write-Output "##### Copy Release APK to project root"
Write-Output "########################################"
$signedApkPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/com.x8bit.bitwarden-Signed.apk");
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden.apk");
$signedApkPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/$($packageName)-Signed.apk");
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).apk");
Copy-Item $signedApkPath $signedApkDestPath
shell: pwsh
- name: Upload Play Store .aab artifact
- name: Upload Prod .aab artifact
if: ${{ matrix.variant == 'prod' }}
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: com.x8bit.bitwarden.aab
path: ./com.x8bit.bitwarden.aab
if-no-files-found: error
- name: Upload Play Store .apk artifact
- name: Upload Prod .apk artifact
if: ${{ matrix.variant == 'prod' }}
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: com.x8bit.bitwarden.apk
path: ./com.x8bit.bitwarden.apk
if-no-files-found: error
- name: Upload Other .apk artifact
if: ${{ matrix.variant != 'prod' }}
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk
if-no-files-found: error
- name: Create checksum for Prod .apk artifact
if: ${{ matrix.variant == 'prod' }}
run: |
checksum -f="./com.x8bit.bitwarden.apk" `
-t sha256 | Out-File -Encoding ASCII ./bw-android-apk-sha256.txt
- name: Create checksum for Other .apk artifact
if: ${{ matrix.variant != 'prod' }}
run: |
checksum -f="./com.x8bit.bitwarden.${{ matrix.variant }}.apk" `
-t sha256 | Out-File -Encoding ASCII ./bw-android-${{ matrix.variant }}-apk-sha256.txt
- name: Upload .apk sha file for prod
if: ${{ matrix.variant == 'prod' }}
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: bw-android-apk-sha256.txt
path: ./bw-android-apk-sha256.txt
if-no-files-found: error
- name: Upload .apk sha file for other
if: ${{ matrix.variant != 'prod' }}
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: bw-android-${{ matrix.variant }}-apk-sha256.txt
path: ./bw-android-${{ matrix.variant }}-apk-sha256.txt
if-no-files-found: error
- name: Deploy to Play Store
if: |
(github.ref == 'refs/heads/master'
&& needs.setup.outputs.rc_branch_exists == 0
&& needs.setup.outputs.hotfix_branch_exists == 0)
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|| github.ref == 'refs/heads/hotfix-rc'
if: ${{ matrix.variant == 'prod' && (( github.ref == 'refs/heads/master'
&& needs.setup.outputs.rc_branch_exists == 0
&& needs.setup.outputs.hotfix_branch_exists == 0)
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|| github.ref == 'refs/heads/hotfix-rc' ) }}
run: |
PUBLISHER_PATH="$GITHUB_WORKSPACE/store/google/Publisher/bin/Release/netcoreapp3.1/Publisher.dll"
CREDS_PATH="$HOME/secrets/play_creds.json"
@@ -235,13 +301,16 @@ jobs:
runs-on: windows-2022
steps:
- name: Setup NuGet
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
with:
nuget-version: 5.9.0
- name: Set up MSBuild
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
- name: Setup Windows builder
run: choco install checksum --no-progress
- name: Work Around for broken Windows 2022 Runner Image
run: |
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
@@ -412,14 +481,26 @@ jobs:
path: ./com.x8bit.bitwarden-fdroid.apk
if-no-files-found: error
- name: Create checksum for F-Droid artifact
run: |
checksum -f="./com.x8bit.bitwarden-fdroid.apk" `
-t sha256 | Out-File -Encoding ASCII ./bw-fdroid-apk-sha256.txt
- name: Upload F-Droid sha file
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: bw-fdroid-apk-sha256.txt
path: ./bw-fdroid-apk-sha256.txt
if-no-files-found: error
ios:
name: Apple iOS
runs-on: macos-11
runs-on: macos-12
needs: setup
steps:
- name: Setup NuGet
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
with:
nuget-version: 5.9.0
@@ -450,7 +531,7 @@ jobs:
do
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$i::$VALUE"
echo "$i=$VALUE" >> $GITHUB_OUTPUT
done
- name: Decrypt secrets
@@ -472,6 +553,14 @@ 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
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output ./src/watchOS/bitwarden/GoogleService-Info.plist ./.github/secrets/GoogleService-Info.plist.gpg
shell: bash
- name: Increment version
@@ -486,6 +575,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
@@ -520,6 +612,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"
@@ -535,6 +629,25 @@ 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 "########################################"
shell: bash
- name: Restore packages
@@ -572,7 +685,12 @@ jobs:
ARCHIVE_DSYMS_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive/dSYMs"
EXPORT_PATH="./bitwarden-export"
cp -r $ARCHIVE_DSYMS_PATH $EXPORT_PATH
WATCH_ARCHIVE_DSYMS_PATH="./src/watchOS/bitwarden.xcarchive/dSYMs/"
WATCH_DSYMS_EXPORT_PATH="$EXPORT_PATH/Watch_dSYMs"
cp -r -v $ARCHIVE_DSYMS_PATH $EXPORT_PATH
mkdir $WATCH_DSYMS_EXPORT_PATH
cp -r -v $WATCH_ARCHIVE_DSYMS_PATH $WATCH_DSYMS_EXPORT_PATH
shell: bash
- name: Upload App Store .ipa & dSYMs artifacts
@@ -595,23 +713,39 @@ jobs:
- name: Upload dSYMs to App Center
if: |
(github.ref == 'refs/heads/master'
&& needs.setup.outputs.rc_branch_exists == 0
&& needs.setup.outputs.hotfix_branch_exists == 0)
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|| github.ref == 'refs/heads/hotfix-rc'
(github.ref == 'refs/heads/master'
&& needs.setup.outputs.rc_branch_exists == 0
&& needs.setup.outputs.hotfix_branch_exists == 0)
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|| github.ref == 'refs/heads/hotfix-rc'
env:
APPCENTER_IOS_TOKEN: ${{ steps.retrieve-secrets.outputs.appcenter-ios-token }}
run: appcenter crashes upload-symbols -a bitwarden/bitwarden -s "./bitwarden-export/dSYMs" --token $APPCENTER_IOS_TOKEN
shell: bash
- name: Upload Watch dSYMs to Firebase Crashlytics
if: |
(github.ref == 'refs/heads/master'
&& needs.setup.outputs.rc_branch_exists == 0
&& needs.setup.outputs.hotfix_branch_exists == 0)
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|| github.ref == 'refs/heads/hotfix-rc'
run: |
echo "########################################"
echo "##### Uploading Watch dSYMs to Firebase"
echo "########################################"
find "$HOME/Library/Developer/XCode/DerivedData" -name "upload-symbols" -exec chmod +x {} \; -exec {} -gsp "./src/watchOS/bitwarden/GoogleService-Info.plist" -p ios "./bitwarden-export/Watch_dSYMs" \;
shell: bash
- name: Deploy to App Store
if: |
(github.ref == 'refs/heads/master'
&& needs.setup.outputs.rc_branch_exists == 0
&& needs.setup.outputs.hotfix_branch_exists == 0)
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|| github.ref == 'refs/heads/hotfix-rc'
(github.ref == 'refs/heads/master'
&& needs.setup.outputs.rc_branch_exists == 0
&& needs.setup.outputs.hotfix_branch_exists == 0)
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|| github.ref == 'refs/heads/hotfix-rc'
env:
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
@@ -651,7 +785,7 @@ jobs:
do
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$i::$VALUE"
echo "$i=$VALUE" >> $GITHUB_OUTPUT
done
- name: Upload Sources
@@ -679,9 +813,9 @@ jobs:
steps:
- name: Check if any job failed
if: |
(github.ref == 'refs/heads/master')
|| (github.ref == 'refs/heads/rc')
|| (github.ref == 'refs/heads/hotfix-rc')
(github.ref == 'refs/heads/master')
|| (github.ref == 'refs/heads/rc')
|| (github.ref == 'refs/heads/hotfix-rc')
env:
CLOC_STATUS: ${{ needs.cloc.result }}
ANDROID_STATUS: ${{ needs.android.result }}
@@ -719,7 +853,7 @@ jobs:
do
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$i::$VALUE"
echo "$i=$VALUE" >> $GITHUB_OUTPUT
done
- name: Notify Slack on failure

View File

@@ -24,17 +24,10 @@ jobs:
- name: Retrieve secrets
id: retrieve-secrets
env:
KEYVAULT: bitwarden-prod-kv
SECRETS: |
crowdin-api-token
run: |
for i in ${SECRETS//,/ }
do
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$i::$VALUE"
done
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
with:
keyvault: "bitwarden-prod-kv"
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Download translations
uses: crowdin/github-action@12143a68c213f3c6d9913c9e5023224f7231face
@@ -47,10 +40,12 @@ jobs:
upload_sources: false
upload_translations: false
download_translations: true
github_user_name: "github-actions"
github_user_email: "<>"
github_user_name: "bitwarden-devops-bot"
github_user_email: "106330231+bitwarden-devops-bot@users.noreply.github.com"
commit_message: "Autosync the updated translations"
localization_branch_name: crowdin-auto-sync
create_pull_request: true
pull_request_title: "Autosync Crowdin Translations"
pull_request_body: "Autosync the updated translations"
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
gpg_passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}

View File

@@ -12,5 +12,5 @@ jobs:
- name: Enforce Label
uses: yogevbd/enforce-label-action@8d1e1709b1011e6d90400a0e6cf7c0b77aa5efeb
with:
BANNED_LABELS: "hold"
BANNED_LABELS_DESCRIPTION: "PRs on hold cannot be merged"
BANNED_LABELS: "hold,needs-qa"
BANNED_LABELS_DESCRIPTION: "PRs with the hold or needs-qa labels cannot be merged"

View File

@@ -1,5 +1,6 @@
---
name: Release
run-name: Release ${{ inputs.release_type }}
on:
workflow_dispatch:
@@ -51,9 +52,10 @@ jobs:
id: branch
run: |
BRANCH_NAME=$(basename ${{ github.ref }})
echo "::set-output name=branch-name::$BRANCH_NAME"
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Create GitHub deployment
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: chrnorm/deployment-action@1b599fe41a0ef1f95191e7f2eec4743f2d7dfc48
id: deployment
with:
@@ -62,7 +64,7 @@ jobs:
environment: 'production'
description: 'Deployment ${{ steps.version.outputs.version }} from branch ${{ steps.branch.outputs.branch-name }}'
task: release
- name: Download all artifacts
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
@@ -72,7 +74,7 @@ jobs:
workflow_conclusion: success
branch: ${{ steps.branch.outputs.branch-name }}
- name: Download all artifacts
- name: Dry Run - Download all artifacts
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
with:
@@ -90,7 +92,9 @@ jobs:
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
./com.x8bit.bitwarden.apk/com.x8bit.bitwarden.apk,
./com.x8bit.bitwarden-fdroid.apk/com.x8bit.bitwarden-fdroid.apk,
./Bitwarden iOS.zip"
./Bitwarden iOS.zip,
./bw-android-apk-sha256.txt,
./bw-fdroid-apk-sha256.txt"
commit: ${{ github.sha }}
tag: v${{ steps.version.outputs.version }}
name: Version ${{ steps.version.outputs.version }}
@@ -99,7 +103,7 @@ jobs:
draft: true
- name: Update deployment status to Success
if: ${{ success() }}
if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }}
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
with:
token: '${{ secrets.GITHUB_TOKEN }}'
@@ -107,7 +111,7 @@ jobs:
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
- name: Update deployment status to Failure
if: ${{ failure() }}
if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }}
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
with:
token: '${{ secrets.GITHUB_TOKEN }}'
@@ -133,7 +137,7 @@ jobs:
branch: ${{ needs.release.outputs.branch-name }}
name: com.x8bit.bitwarden-fdroid.apk
- name: Download F-Droid .apk artifact
- name: Dry Run - Download F-Droid .apk artifact
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
with:

View File

@@ -2,39 +2,38 @@
name: Version Auto Bump
on:
release:
types: [published]
push:
tags:
- v**
jobs:
setup:
name: "Setup"
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
outputs:
version_number: ${{ steps.version.outputs.new-version }}
steps:
- name: Checkout Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Get version to bump
- name: Calculate bumped version
id: version
env:
RELEASE_TAG: ${{ github.event.release.tag }}
RELEASE_TAG: ${{ github.ref }}
run: |
CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]\.)([0-9])/\1/')
CURR_VER=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]\.)([0-9])/\2/')
echo $CURR_VER
CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/refs\/tags\/v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\1/')
CURR_PATCH=$(echo $RELEASE_TAG | sed -r 's/refs\/tags\/v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\2/')
echo "Current Major: $CURR_MAJOR"
echo "Current Patch: $CURR_PATCH"
((CURR_VER++))
NEW_VER=$CURR_MAJOR$CURR_VER
echo $NEW_VER
echo "::set-output name=new-version::$NEW_VER"
NEW_PATCH=$((CURR_PATCH+1))
NEW_VER=$CURR_MAJOR.$NEW_PATCH
echo "New Version: $NEW_VER"
echo "new-version=$NEW_VER" >> $GITHUB_OUTPUT
trigger_version_bump:
name: "Trigger version bump workflow"
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs:
- setup
steps:
@@ -45,13 +44,10 @@ jobs:
- name: Retrieve secrets
id: retrieve-secrets
env:
KEYVAULT: bitwarden-prod-kv
SECRET: "github-pat-bitwarden-devops-bot-repo-scope"
run: |
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $SECRET --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$SECRET::$VALUE"
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
with:
keyvault: "bitwarden-prod-kv"
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Call GitHub API to trigger workflow bump
env:

View File

@@ -16,9 +16,28 @@ jobs:
- name: Checkout Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Login to Azure - Prod Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
with:
keyvault: "bitwarden-prod-kv"
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@c8bb57c57e8df1be8c73ff3d59deab1dbc00e0d1
with:
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
git_user_signingkey: true
git_commit_gpgsign: true
- name: Create Version Branch
run: |
git switch -c version_bump_${{ github.event.inputs.version_number }}
run: git switch -c version_bump_${{ github.event.inputs.version_number }}
- name: Bump Version - Android XML
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
@@ -52,23 +71,22 @@ jobs:
- name: Setup git
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
- name: Check if version changed
id: version-changed
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "::set-output name=changes_to_commit::TRUE"
echo "changes_to_commit=TRUE" >> $GITHUB_OUTPUT
else
echo "::set-output name=changes_to_commit::FALSE"
echo "changes_to_commit=FALSE" >> $GITHUB_OUTPUT
echo "No changes to commit!";
fi
- name: Commit files
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
run: |
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
run: git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
- name: Push changes
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}

129
.gitignore vendored
View File

@@ -30,6 +30,7 @@ Components/
[Rr]eleases/
x64/
x86/
!src/lib/x86/
build/
bld/
[Bb]in/
@@ -208,4 +209,130 @@ FakesAssemblies/
# Other
project.lock.json
.DS_Store
src/App/Css
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

@@ -12,7 +12,7 @@ The Bitwarden mobile application is written in C# with Xamarin Android, Xamarin
# Build/Run
Please refer to the [Mobile section](https://contributing.bitwarden.com/mobile/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.
Please refer to the [Mobile section](https://contributing.bitwarden.com/getting-started/clients/mobile/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.
# We're Hiring!

346
build.cake Normal file
View File

@@ -0,0 +1,346 @@
#addin nuget:?package=Cake.FileHelpers&version=5.0.0
#addin nuget:?package=Cake.AndroidAppManifest&version=1.1.2
#addin nuget:?package=Cake.Plist&version=0.7.0
#addin nuget:?package=Cake.Incubator&version=7.0.0
#tool dotnet:?package=GitVersion.Tool&version=5.10.3
using Path = System.IO.Path;
var debugScript = Argument<bool>("debugScript", false);
var target = Argument("target", "Default");
var configuration = Argument("configuration", "Release");
var variant = Argument("variant", "dev");
abstract record VariantConfig(
string AppName,
string AndroidPackageName,
string iOSBundleId,
string ApsEnvironment
);
const string BASE_BUNDLE_ID_DROID = "com.x8bit.bitwarden";
const string BASE_BUNDLE_ID_IOS = "com.8bit.bitwarden";
record Dev(): VariantConfig("Bitwarden Dev", $"{BASE_BUNDLE_ID_DROID}.dev", $"{BASE_BUNDLE_ID_IOS}.dev", "development");
record QA(): VariantConfig("Bitwarden QA", $"{BASE_BUNDLE_ID_DROID}.qa", $"{BASE_BUNDLE_ID_IOS}.qa", "development");
record Beta(): VariantConfig("Bitwarden Beta", $"{BASE_BUNDLE_ID_DROID}.beta", $"{BASE_BUNDLE_ID_IOS}.beta", "production");
record Prod(): VariantConfig("Bitwarden", $"{BASE_BUNDLE_ID_DROID}", $"{BASE_BUNDLE_ID_IOS}", "production");
VariantConfig GetVariant() => variant.ToLower() switch{
"qa" => new QA(),
"beta" => new Beta(),
"prod" => new Prod(),
_ => new Dev()
};
GitVersion _gitVersion; //will be set by GetGitInfo task
var _slnPath = Path.Combine(""); //base path used to access files. If build.cake file is moved, just update this
string _androidPackageName = string.Empty; //will be set by UpdateAndroidManifest task
string CreateFeatureBranch(string prevVersionName, GitVersion git) => $"{prevVersionName}-{git.BranchName.Replace("/","-")}";
string GetVersionName(string prevVersionName, VariantConfig buildVariant, GitVersion git) => buildVariant is Prod? prevVersionName : CreateFeatureBranch(prevVersionName, git);
int CreateBuildNumber(int previousNumber) => ++previousNumber;
Task("GetGitInfo")
.Does(()=> {
_gitVersion = GitVersion(new GitVersionSettings());
if(debugScript)
{
Information($"GitVersion Dump:\n{_gitVersion.Dump()}");
}
Information("Git data Load successfully.");
});
#region Android
Task("UpdateAndroidAppIcon")
.Does(()=>{
//TODO we'll implement variant icons later
//manifest.ApplicationIcon = "@mipmap/ic_launcher";
Information($"Updated Androix App Icon with success");
});
Task("UpdateAndroidManifest")
.IsDependentOn("GetGitInfo")
.Does(()=>
{
var buildVariant = GetVariant();
var manifestPath = Path.Combine(_slnPath, "src", "Android", "Properties", "AndroidManifest.xml");
// Cake.AndroidAppManifest doesn't currently enable us to access nested items so, quick (not ideal) fix:
var manifestText = FileReadText(manifestPath);
manifestText = manifestText.Replace("com.x8bit.bitwarden.", buildVariant.AndroidPackageName + ".");
manifestText = manifestText.Replace("android:label=\"Bitwarden\"", $"android:label=\"{buildVariant.AppName}\"");
FileWriteText(manifestPath, manifestText);
var manifest = DeserializeAppManifest(manifestPath);
var prevVersionCode = manifest.VersionCode;
var prevVersionName = manifest.VersionName;
_androidPackageName = manifest.PackageName;
//manifest.VersionCode = CreateBuildNumber(prevVersionCode);
manifest.VersionName = GetVersionName(prevVersionName, buildVariant, _gitVersion);
manifest.PackageName = buildVariant.AndroidPackageName;
manifest.ApplicationLabel = buildVariant.AppName;
//Information($"AndroidManigest.xml VersionCode from {prevVersionCode} to {manifest.VersionCode}");
Information($"AndroidManigest.xml VersionName from {prevVersionName} to {manifest.VersionName}");
Information($"AndroidManigest.xml PackageName from {_androidPackageName} to {buildVariant.AndroidPackageName}");
Information($"AndroidManigest.xml ApplicationLabel to {buildVariant.AppName}");
SerializeAppManifest(manifestPath, manifest);
Information("AndroidManifest updated with success!");
});
void ReplaceInFile(string filePath, string oldtext, string newtext)
{
var fileText = FileReadText(filePath);
if(string.IsNullOrEmpty(fileText) || !fileText.Contains(oldtext))
{
throw new Exception($"Couldn't find {filePath} or it didn't contain: {oldtext}");
}
fileText = fileText.Replace(oldtext, newtext);
FileWriteText(filePath, fileText);
Information($"{filePath} modified successfully.");
}
Task("UpdateAndroidCodeFiles")
.IsDependentOn("UpdateAndroidManifest")
.Does(()=> {
var buildVariant = GetVariant();
//We're not using _androidPackageName here because the codefile is currently slightly different string than the one in AndroidManifest.xml
var keyName = "com.8bit.bitwarden";
var fixedPackageName = buildVariant.AndroidPackageName.Replace("x8bit", "8bit");
var filePath = Path.Combine(_slnPath, "src", "Android", "Services", "BiometricService.cs");
ReplaceInFile(filePath, keyName, fixedPackageName);
var packageFileList = new string[] {
Path.Combine(_slnPath, "src", "Android", "MainActivity.cs"),
Path.Combine(_slnPath, "src", "Android", "MainApplication.cs"),
Path.Combine(_slnPath, "src", "Android", "Constants.cs"),
Path.Combine(_slnPath, "src", "Android", "Accessibility", "AccessibilityService.cs"),
Path.Combine(_slnPath, "src", "Android", "Autofill", "AutofillHelpers.cs"),
Path.Combine(_slnPath, "src", "Android", "Autofill", "AutofillService.cs"),
Path.Combine(_slnPath, "src", "Android", "Receivers", "ClearClipboardAlarmReceiver.cs"),
Path.Combine(_slnPath, "src", "Android", "Receivers", "EventUploadReceiver.cs"),
Path.Combine(_slnPath, "src", "Android", "Receivers", "PackageReplacedReceiver.cs"),
Path.Combine(_slnPath, "src", "Android", "Receivers", "RestrictionsChangedReceiver.cs"),
Path.Combine(_slnPath, "src", "Android", "Services", "DeviceActionService.cs"),
Path.Combine(_slnPath, "src", "Android", "Services", "FileService.cs"),
Path.Combine(_slnPath, "src", "Android", "Tiles", "AutofillTileService.cs"),
Path.Combine(_slnPath, "src", "Android", "Tiles", "GeneratorTileService.cs"),
Path.Combine(_slnPath, "src", "Android", "Tiles", "MyVaultTileService.cs"),
Path.Combine(_slnPath, "src", "Android", "google-services.json"),
Path.Combine(_slnPath, "store", "google", "Publisher", "Program.cs"),
};
foreach(string path in packageFileList)
{
ReplaceInFile(path, "com.x8bit.bitwarden", buildVariant.AndroidPackageName);
}
var labelFileList = new string[] {
Path.Combine(_slnPath, "src", "Android", "Autofill", "AutofillService.cs"),
};
foreach(string path in labelFileList)
{
ReplaceInFile(path, "Bitwarden\"", $"{buildVariant.AppName}\"");
}
});
#endregion Android
#region iOS
enum iOSProjectType
{
Null,
MainApp,
Autofill,
Extension,
ShareExtension
}
string GetiOSBundleId(VariantConfig buildVariant, iOSProjectType projectType) => projectType switch
{
iOSProjectType.Autofill => $"{buildVariant.iOSBundleId}.autofill",
iOSProjectType.Extension => $"{buildVariant.iOSBundleId}.find-login-action-extension",
iOSProjectType.ShareExtension => $"{buildVariant.iOSBundleId}.share-extension",
_ => buildVariant.iOSBundleId
};
string GetiOSBundleName(VariantConfig buildVariant, iOSProjectType projectType) => projectType switch
{
iOSProjectType.Autofill => $"{buildVariant.AppName} Autofill",
iOSProjectType.Extension => $"{buildVariant.AppName} Extension",
iOSProjectType.ShareExtension => $"{buildVariant.AppName} Share Extension",
_ => buildVariant.AppName
};
private void UpdateiOSInfoPlist(string plistPath, VariantConfig buildVariant, GitVersion git, iOSProjectType projectType = iOSProjectType.MainApp)
{
var plistFile = File(plistPath);
dynamic plist = DeserializePlist(plistFile);
var prevVersionName = plist["CFBundleShortVersionString"];
var prevVersionString = plist["CFBundleVersion"];
var prevVersion = int.Parse(plist["CFBundleVersion"]);
var prevBundleId = plist["CFBundleIdentifier"];
var prevBundleName = plist["CFBundleName"];
//var newVersion = CreateBuildNumber(prevVersion).ToString();
var newVersionName = GetVersionName(prevVersionName, buildVariant, git);
var newBundleId = GetiOSBundleId(buildVariant, projectType);
var newBundleName = GetiOSBundleName(buildVariant, projectType);
plist["CFBundleName"] = newBundleName;
plist["CFBundleDisplayName"] = newBundleName;
//plist["CFBundleVersion"] = newVersion;
plist["CFBundleShortVersionString"] = newVersionName;
plist["CFBundleIdentifier"] = newBundleId;
if(projectType == iOSProjectType.MainApp)
{
plist["CFBundleURLTypes"][0]["CFBundleURLName"] = $"{buildVariant.iOSBundleId}.url";
}
if(projectType == iOSProjectType.Extension)
{
var keyText = plist["NSExtension"]["NSExtensionAttributes"]["NSExtensionActivationRule"];
plist["NSExtension"]["NSExtensionAttributes"]["NSExtensionActivationRule"] = keyText.Replace("com.8bit.bitwarden", buildVariant.iOSBundleId);
}
SerializePlist(plistFile, plist);
Information($"Changed app name from {prevBundleName} to {newBundleName}");
//Information($"Changed Bundle Version from {prevVersion} to {newVersion}");
Information($"Changed Bundle Short Version name from {prevVersionName} to {newVersionName}");
Information($"Changed Bundle Identifier from {prevBundleId} to {newBundleId}");
Information($"{plistPath} updated with success!");
}
private void UpdateiOSEntitlementsPlist(string entitlementsPath, VariantConfig buildVariant)
{
var EntitlementlistFile = File(entitlementsPath);
dynamic Entitlements = DeserializePlist(EntitlementlistFile);
Entitlements["aps-environment"] = buildVariant.ApsEnvironment;
Entitlements["keychain-access-groups"] = new List<string>() { "$(AppIdentifierPrefix)" + buildVariant.iOSBundleId };
Entitlements["com.apple.security.application-groups"] = new List<string>() { $"group.{buildVariant.iOSBundleId}" };;
Information($"Changed ApsEnvironment name to {buildVariant.ApsEnvironment}");
Information($"Changed keychain-access-groups bundleID to {buildVariant.iOSBundleId}");
SerializePlist(EntitlementlistFile, Entitlements);
Information($"{entitlementsPath} updated with success!");
}
Task("UpdateiOSIcon")
.Does(()=>{
//TODO we'll implement variant icons later
Information($"Updating IOS App Icon");
});
Task("UpdateiOSPlist")
.IsDependentOn("GetGitInfo")
.Does(()=> {
var buildVariant = GetVariant();
var infoPath = Path.Combine(_slnPath, "src", "iOS", "Info.plist");
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS", "Entitlements.plist");
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.MainApp);
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant);
});
Task("UpdateiOSAutofillPlist")
.IsDependentOn("GetGitInfo")
.IsDependentOn("UpdateiOSPlist")
.Does(()=> {
var buildVariant = GetVariant();
var infoPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Info.plist");
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Entitlements.plist");
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Autofill);
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant);
});
Task("UpdateiOSExtensionPlist")
.IsDependentOn("GetGitInfo")
.IsDependentOn("UpdateiOSPlist")
.Does(()=> {
var buildVariant = GetVariant();
var infoPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Info.plist");
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Entitlements.plist");
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Extension);
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant);
});
Task("UpdateiOSShareExtensionPlist")
.IsDependentOn("GetGitInfo")
.IsDependentOn("UpdateiOSPlist")
.Does(()=> {
var buildVariant = GetVariant();
var infoPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Info.plist");
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Entitlements.plist");
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.ShareExtension);
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant);
});
Task("UpdateiOSCodeFiles")
.IsDependentOn("UpdateiOSPlist")
.Does(()=> {
var buildVariant = GetVariant();
var fileList = new string[] {
Path.Combine(_slnPath, "src", "iOS.Core", "Utilities", "iOSCoreHelpers.cs"),
Path.Combine(_slnPath, "src", "iOS.Core", "Constants.cs"),
Path.Combine(".github", "resources", "export-options-ad-hoc.plist"),
Path.Combine(".github", "resources", "export-options-app-store.plist"),
};
foreach(string path in fileList)
{
ReplaceInFile(path, "com.8bit.bitwarden", buildVariant.iOSBundleId);
}
});
#endregion iOS
#region Main Tasks
Task("Android")
//.IsDependentOn("UpdateAndroidAppIcon")
.IsDependentOn("UpdateAndroidManifest")
.IsDependentOn("UpdateAndroidCodeFiles")
.Does(()=>
{
Information("Android app updated");
});
Task("iOS")
//.IsDependentOn("UpdateiOSIcon")
.IsDependentOn("UpdateiOSPlist")
.IsDependentOn("UpdateiOSAutofillPlist")
.IsDependentOn("UpdateiOSExtensionPlist")
.IsDependentOn("UpdateiOSShareExtensionPlist")
.IsDependentOn("UpdateiOSCodeFiles")
.Does(()=>
{
Information("iOS app updated");
});
Task("Default")
.Does(() => {
var usage = @"Missing target.
Usage:
dotnet cake build.cake --target (Android | iOS) --variant (dev | qa | beta | prod)
Options:
--debugScript=<bool> Script debug mode.
";
Information(usage);
});
#endregion Main Tasks
RunTarget(target);

BIN
lib/ios/libargon2.a Normal file

Binary file not shown.

View File

@@ -33,6 +33,7 @@ namespace Bit.Droid.Accessibility
// - Resources/xml/autofillservice.xml
new Browser("alook.browser", "search_fragment_input_view"),
new Browser("alook.browser.google", "search_fragment_input_view"),
new Browser("app.vanadium.browser", "url_bar"),
new Browser("com.amazon.cloud9", "url"),
new Browser("com.android.browser", "url"),
new Browser("com.android.chrome", "url_bar"),
@@ -66,6 +67,7 @@ namespace Bit.Droid.Accessibility
new Browser("com.mmbox.xbrowser", "search_box"),
new Browser("com.mycompany.app.soulbrowser", "edit_text"),
new Browser("com.naver.whale", "url_bar"),
new Browser("com.neeva.app", "full_url_text_view"),
new Browser("com.opera.browser", "url_field"),
new Browser("com.opera.browser.beta", "url_field"),
new Browser("com.opera.gx", "addressbarEdit"),
@@ -74,6 +76,7 @@ namespace Bit.Droid.Accessibility
new Browser("com.opera.touch", "addressbarEdit"),
new Browser("com.qflair.browserq", "url"),
new Browser("com.qwant.liberty", "mozac_browser_toolbar_url_view,url_bar_title"), // 2nd = Legacy (before v4)
new Browser("com.rainsee.create", "search_box"),
new Browser("com.sec.android.app.sbrowser", "location_bar_edit_text"),
new Browser("com.sec.android.app.sbrowser.beta", "location_bar_edit_text"),
new Browser("com.stoutner.privacybrowser.free", "url_edittext"),
@@ -83,6 +86,9 @@ namespace Bit.Droid.Accessibility
new Browser("com.vivaldi.browser.sopranos", "url_bar"),
new Browser("com.yandex.browser", "bro_omnibar_address_title_text,bro_omnibox_collapsed_title",
(s) => s.Split(new char[]{' ', ' '}).FirstOrDefault()), // 0 = Regular Space, 1 = No-break space (00A0)
new Browser("com.yjllq.internet", "search_box"),
new Browser("com.yjllq.kito", "search_box"),
new Browser("com.yujian.ResideMenuDemo", "search_box"),
new Browser("com.z28j.feel", "g2"),
new Browser("idm.internet.download.manager", "search"),
new Browser("idm.internet.download.manager.adm.lite", "search"),
@@ -90,6 +96,7 @@ namespace Bit.Droid.Accessibility
new Browser("io.github.forkmaintainers.iceraven", "mozac_browser_toolbar_url_view"),
new Browser("mark.via", "am,an"),
new Browser("mark.via.gp", "as"),
new Browser("net.dezor.browser", "url_bar"),
new Browser("net.slions.fulguris.full.download", "search"),
new Browser("net.slions.fulguris.full.download.debug", "search"),
new Browser("net.slions.fulguris.full.playstore", "search"),
@@ -129,6 +136,7 @@ namespace Bit.Droid.Accessibility
new Browser("com.htc.sense.browser", "title"),
new Browser("com.jerky.browser2", "enterUrl"),
new Browser("com.ksmobile.cb", "address_bar_edit_text"),
new Browser("com.lemurbrowser.exts","url_bar"),
new Browser("com.linkbubble.playstore", "url_text"),
new Browser("com.mx.browser", "address_editor_with_progress"),
new Browser("com.mx.browser.tablet", "address_editor_with_progress"),
@@ -367,7 +375,7 @@ namespace Bit.Droid.Accessibility
public static string GetUri(AccessibilityNodeInfo root)
{
var uri = string.Concat(Constants.AndroidAppProtocol, root.PackageName);
var uri = string.Concat(Core.Constants.AndroidAppProtocol, root.PackageName);
if (SupportedBrowsers.ContainsKey(root.PackageName))
{
var browser = SupportedBrowsers[root.PackageName];

View File

@@ -15,7 +15,7 @@
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
<TargetFrameworkVersion>v12.1</TargetFrameworkVersion>
<TargetFrameworkVersion>v13.0</TargetFrameworkVersion>
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
@@ -77,12 +77,12 @@
<PackageReference Include="Portable.BouncyCastle">
<Version>1.9.0</Version>
</PackageReference>
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.5.1" />
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.13" />
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.16" />
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.9.0" />
<PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.14" />
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.3.1" />
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.5.1.1" />
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.14" />
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.17" />
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.9.0.1" />
<PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.15" />
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.3.1.1" />
<PackageReference Include="Xamarin.Essentials">
<Version>1.7.3</Version>
</PackageReference>
@@ -103,8 +103,10 @@
<Compile Include="Accessibility\Browser.cs" />
<Compile Include="Accessibility\NodeList.cs" />
<Compile Include="Accessibility\KnownUsernameField.cs" />
<Compile Include="Autofill\AutofillConstants.cs" />
<Compile Include="Autofill\AutofillHelpers.cs" />
<Compile Include="Autofill\AutofillService.cs" />
<Compile Include="Autofill\AutofillExternalSelectionActivity.cs" />
<Compile Include="Autofill\Field.cs" />
<Compile Include="Autofill\FieldCollection.cs" />
<Compile Include="Autofill\FilledItem.cs" />
@@ -152,6 +154,12 @@
<Compile Include="Utilities\IntentExtensions.cs" />
<Compile Include="Renderers\CustomPageRenderer.cs" />
<Compile Include="Effects\NoEmojiKeyboardEffect.cs" />
<Compile Include="Receivers\NotificationDismissReceiver.cs" />
<Compile Include="Services\FileService.cs" />
<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" />
@@ -161,6 +169,10 @@
<GoogleServicesJson Include="google-services.json" />
<GoogleServicesJson Include="google-services.json.enc" />
<None Include="fdroid-keystore.jks.enc" />
<AndroidNativeLibrary Include="lib\arm64-v8a\libargon2.so" />
<AndroidNativeLibrary Include="lib\armeabi-v7a\libargon2.so" />
<AndroidNativeLibrary Include="lib\x86\libargon2.so" />
<AndroidNativeLibrary Include="lib\x86_64\libargon2.so" />
<None Include="Properties\AndroidManifest.xml" />
<None Include="upload-keystore.jks.enc" />
</ItemGroup>

Binary file not shown.

View File

@@ -0,0 +1,10 @@
namespace Bit.Droid.Autofill
{
public class AutofillConstants
{
public const string AutofillFramework = "autofillFramework";
public const string AutofillFrameworkFillType = "autofillFrameworkFillType";
public const string AutofillFrameworkUri = "autofillFrameworkUri";
public const string AutofillFrameworkCipherId = "autofillFrameworkCipherId";
}
}

View File

@@ -0,0 +1,42 @@
using System.Threading.Tasks;
using Android.App;
using Android.Content.PM;
using Android.OS;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Bit.Droid.Utilities;
namespace Bit.Droid.Autofill
{
[Activity(
NoHistory = true,
LaunchMode = LaunchMode.SingleTop)]
public class AutofillExternalSelectionActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
protected override void OnCreate(Bundle bundle)
{
Intent?.Validate();
base.OnCreate(bundle);
var cipherId = Intent?.GetStringExtra(AutofillConstants.AutofillFrameworkCipherId);
if (string.IsNullOrEmpty(cipherId))
{
SetResult(Result.Canceled);
Finish();
return;
}
GetCipherAndPerformAutofillAsync(cipherId).FireAndForget();
}
private async Task GetCipherAndPerformAutofillAsync(string cipherId)
{
var cipherService = ServiceContainer.Resolve<ICipherService>();
var cipher = await cipherService.GetAsync(cipherId);
var decCipher = await cipher.DecryptAsync();
var autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
autofillHandler.Autofill(decCipher);
}
}
}

View File

@@ -54,6 +54,7 @@ namespace Bit.Droid.Autofill
{
"alook.browser",
"alook.browser.google",
"app.vanadium.browser",
"com.amazon.cloud9",
"com.android.browser",
"com.android.chrome",
@@ -78,6 +79,7 @@ namespace Bit.Droid.Autofill
"com.jamal2367.styx",
"com.kiwibrowser.browser",
"com.kiwibrowser.browser.dev",
"com.lemurbrowser.exts",
"com.microsoft.emmx",
"com.microsoft.emmx.beta",
"com.microsoft.emmx.canary",
@@ -86,6 +88,7 @@ namespace Bit.Droid.Autofill
"com.mmbox.xbrowser",
"com.mycompany.app.soulbrowser",
"com.naver.whale",
"com.neeva.app",
"com.opera.browser",
"com.opera.browser.beta",
"com.opera.gx",
@@ -94,6 +97,7 @@ namespace Bit.Droid.Autofill
"com.opera.touch",
"com.qflair.browserq",
"com.qwant.liberty",
"com.rainsee.create",
"com.sec.android.app.sbrowser",
"com.sec.android.app.sbrowser.beta",
"com.stoutner.privacybrowser.free",
@@ -102,6 +106,9 @@ namespace Bit.Droid.Autofill
"com.vivaldi.browser.snapshot",
"com.vivaldi.browser.sopranos",
"com.yandex.browser",
"com.yjllq.internet",
"com.yjllq.kito",
"com.yujian.ResideMenuDemo",
"com.z28j.feel",
"idm.internet.download.manager",
"idm.internet.download.manager.adm.lite",
@@ -109,6 +116,7 @@ namespace Bit.Droid.Autofill
"io.github.forkmaintainers.iceraven",
"mark.via",
"mark.via.gp",
"net.dezor.browser",
"net.slions.fulguris.full.download",
"net.slions.fulguris.full.download.debug",
"net.slions.fulguris.full.playstore",
@@ -207,7 +215,7 @@ namespace Bit.Droid.Autofill
}
}
var dataset = BuildDataset(parser.ApplicationContext, parser.FieldCollection, items[i],
inlinePresentationSpec);
true, inlinePresentationSpec);
if (dataset != null)
{
responseBuilder.AddDataset(dataset);
@@ -221,7 +229,7 @@ namespace Bit.Droid.Autofill
}
public static Dataset BuildDataset(Context context, FieldCollection fields, FilledItem filledItem,
InlinePresentationSpec inlinePresentationSpec = null)
bool includeAuthIntent, InlinePresentationSpec inlinePresentationSpec = null)
{
var overlayPresentation = BuildOverlayPresentation(
filledItem.Name,
@@ -242,6 +250,15 @@ namespace Bit.Droid.Autofill
{
datasetBuilder.SetInlinePresentation(inlinePresentation);
}
if (includeAuthIntent)
{
var intent = new Intent(context, typeof(AutofillExternalSelectionActivity));
intent.PutExtra(AutofillConstants.AutofillFramework, true);
intent.PutExtra(AutofillConstants.AutofillFrameworkCipherId, filledItem.Id);
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent,
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true));
datasetBuilder.SetAuthentication(pendingIntent?.IntentSender);
}
if (filledItem.ApplyToFields(fields, datasetBuilder))
{
return datasetBuilder.Build();
@@ -253,25 +270,26 @@ namespace Bit.Droid.Autofill
IList<InlinePresentationSpec> inlinePresentationSpecs = null)
{
var intent = new Intent(context, typeof(MainActivity));
intent.PutExtra("autofillFramework", true);
intent.PutExtra(AutofillConstants.AutofillFramework, true);
if (fields.FillableForLogin)
{
intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Login);
intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Login);
}
else if (fields.FillableForCard)
{
intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Card);
intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Card);
}
else if (fields.FillableForIdentity)
{
intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Identity);
intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Identity);
}
else
{
return null;
}
intent.PutExtra("autofillFrameworkUri", uri);
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent, AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true));
intent.PutExtra(AutofillConstants.AutofillFrameworkUri, uri);
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent,
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true));
var overlayPresentation = BuildOverlayPresentation(
AppResources.AutofillWithBitwarden,

View File

@@ -134,7 +134,7 @@ namespace Bit.Droid.Autofill
{
case CipherType.Login:
intent.PutExtra("autofillFrameworkName", parser.Uri
.Replace(Constants.AndroidAppProtocol, string.Empty)
.Replace(Core.Constants.AndroidAppProtocol, string.Empty)
.Replace("https://", string.Empty)
.Replace("http://", string.Empty));
intent.PutExtra("autofillFrameworkUri", parser.Uri);

View File

@@ -12,6 +12,7 @@ namespace Bit.Droid.Autofill
private List<Field> _passwordFields = null;
private List<Field> _usernameFields = null;
private HashSet<string> _ignoreSearchTerms = new HashSet<string> { "search", "find", "recipient", "edit" };
private HashSet<string> _usernameTerms = new HashSet<string> { "email", "phone", "username"};
private HashSet<string> _passwordTerms = new HashSet<string> { "password", "pswd" };
public List<AutofillId> AutofillIds { get; private set; } = new List<AutofillId>();
@@ -98,6 +99,11 @@ namespace Bit.Droid.Autofill
_usernameFields.Add(usernameField);
}
}
if (!_usernameFields.Any())
{
_usernameFields = Fields.Where(f => FieldIsUsername(f)).ToList();
}
}
return _usernameFields;
}
@@ -321,13 +327,23 @@ namespace Bit.Droid.Autofill
}
return inputTypePassword && !ValueContainsAnyTerms(f.IdEntry, _ignoreSearchTerms) &&
!ValueContainsAnyTerms(f.Hint, _ignoreSearchTerms);
!ValueContainsAnyTerms(f.Hint, _ignoreSearchTerms) && !FieldIsUsername(f);
}
private bool FieldHasPasswordTerms(Field f)
{
return ValueContainsAnyTerms(f.IdEntry, _passwordTerms) || ValueContainsAnyTerms(f.Hint, _passwordTerms);
}
private bool FieldIsUsername(Field f)
{
return f.InputType.HasFlag(InputTypes.TextVariationWebEmailAddress) || FieldHasUsernameTerms(f);
}
private bool FieldHasUsernameTerms(Field f)
{
return ValueContainsAnyTerms(f.IdEntry, _usernameTerms) || ValueContainsAnyTerms(f.Hint, _usernameTerms);
}
private bool ValueContainsAnyTerms(string value, HashSet<string> terms)
{
@@ -339,4 +355,4 @@ namespace Bit.Droid.Autofill
return terms.Any(t => lowerValue.Contains(t));
}
}
}
}

View File

@@ -23,6 +23,7 @@ namespace Bit.Droid.Autofill
public FilledItem(CipherView cipher)
{
Id = cipher.Id;
Name = cipher.Name;
Type = cipher.Type;
Subtitle = cipher.SubTitle;
@@ -55,6 +56,7 @@ namespace Bit.Droid.Autofill
}
}
public string Id { get; set; }
public string Name { get; set; }
public string Subtitle { get; set; } = string.Empty;
public int Icon { get; set; } = Resource.Drawable.login;

View File

@@ -48,7 +48,7 @@ namespace Bit.Droid.Autofill
}
else
{
_uri = string.Concat(Constants.AndroidAppProtocol, PackageName);
_uri = string.Concat(Core.Constants.AndroidAppProtocol, PackageName);
}
return _uri;
}

7
src/Android/Constants.cs Normal file
View File

@@ -0,0 +1,7 @@
namespace Bit.Droid
{
public static class Constants
{
public const string PACKAGE_NAME = "com.x8bit.bitwarden";
}
}

View File

@@ -0,0 +1,23 @@
using Android.Widget;
using Bit.Droid.Effects;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportEffect(typeof(RemoveFontPaddingEffect), nameof(RemoveFontPaddingEffect))]
namespace Bit.Droid.Effects
{
public class RemoveFontPaddingEffect : PlatformEffect
{
protected override void OnAttached()
{
if (Control is TextView textView)
{
textView.SetIncludeFontPadding(false);
}
}
protected override void OnDetached()
{
}
}
}

View File

@@ -18,8 +18,11 @@ using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Bit.Droid.Autofill;
using Bit.Droid.Receivers;
using Bit.Droid.Utilities;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xamarin.Essentials;
using ZXing.Net.Mobile.Android;
using FileProvider = AndroidX.Core.Content.FileProvider;
@@ -34,11 +37,13 @@ namespace Bit.Droid
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
private IDeviceActionService _deviceActionService;
private IFileService _fileService;
private IMessagingService _messagingService;
private IBroadcasterService _broadcasterService;
private IStateService _stateService;
private IAppIdService _appIdService;
private IEventService _eventService;
private IPushNotificationListenerService _pushNotificationListenerService;
private ILogger _logger;
private PendingIntent _eventUploadPendingIntent;
private AppOptions _appOptions;
@@ -56,11 +61,13 @@ namespace Bit.Droid
StrictMode.SetThreadPolicy(policy);
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_fileService = ServiceContainer.Resolve<IFileService>();
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
_pushNotificationListenerService = ServiceContainer.Resolve<IPushNotificationListenerService>();
_logger = ServiceContainer.Resolve<ILogger>("logger");
TabLayoutResource = Resource.Layout.Tabbar;
@@ -145,6 +152,15 @@ namespace Bit.Droid
AndroidHelpers.SetPreconfiguredRestrictionSettingsAsync(this)
.GetAwaiter()
.GetResult();
if (Intent?.GetStringExtra(Core.Constants.NotificationData) is string notificationDataJson)
{
var notificationType = JToken.Parse(notificationDataJson).SelectToken(Core.Constants.NotificationDataType);
if (notificationType.ToString() == PasswordlessNotificationData.TYPE)
{
_pushNotificationListenerService.OnNotificationTapped(JsonConvert.DeserializeObject<PasswordlessNotificationData>(notificationDataJson)).FireAndForget();
}
}
}
protected override void OnNewIntent(Intent intent)
@@ -198,13 +214,13 @@ namespace Bit.Droid
public async override void OnRequestPermissionsResult(int requestCode, string[] permissions,
[GeneratedEnum] Permission[] grantResults)
{
if (requestCode == Constants.SelectFilePermissionRequestCode)
if (requestCode == Core.Constants.SelectFilePermissionRequestCode)
{
if (grantResults.Any(r => r != Permission.Granted))
{
_messagingService.Send("selectFileCameraPermissionDenied");
}
await _deviceActionService.SelectFileAsync();
await _fileService.SelectFileAsync();
}
else
{
@@ -217,7 +233,7 @@ namespace Bit.Droid
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
if (resultCode == Result.Ok &&
(requestCode == Constants.SelectFileRequestCode || requestCode == Constants.SaveFileRequestCode))
(requestCode == Core.Constants.SelectFileRequestCode || requestCode == Core.Constants.SaveFileRequestCode))
{
Android.Net.Uri uri = null;
string fileName = null;
@@ -239,7 +255,7 @@ namespace Bit.Droid
return;
}
if (requestCode == Constants.SaveFileRequestCode)
if (requestCode == Core.Constants.SaveFileRequestCode)
{
_messagingService.Send("selectSaveFileResult",
new Tuple<string, string>(uri.ToString(), fileName));
@@ -307,13 +323,13 @@ namespace Bit.Droid
{
var options = new AppOptions
{
Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra("autofillFrameworkUri"),
Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra(AutofillConstants.AutofillFrameworkUri),
MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false),
GeneratorTile = Intent.GetBooleanExtra("generatorTile", false),
FromAutofillFramework = Intent.GetBooleanExtra("autofillFramework", false),
FromAutofillFramework = Intent.GetBooleanExtra(AutofillConstants.AutofillFramework, false),
CreateSend = GetCreateSendRequest(Intent)
};
var fillType = Intent.GetIntExtra("autofillFrameworkFillType", 0);
var fillType = Intent.GetIntExtra(AutofillConstants.AutofillFrameworkFillType, 0);
if (fillType > 0)
{
options.FillType = (CipherType)fillType;
@@ -420,7 +436,7 @@ namespace Bit.Droid
return;
}
var channel = new NotificationChannel(Constants.AndroidNotificationChannelId, AppResources.AllNotifications, NotificationImportance.Default);
var channel = new NotificationChannel(Core.Constants.AndroidNotificationChannelId, AppResources.AllNotifications, NotificationImportance.Default);
if(GetSystemService(NotificationService) is NotificationManager notificationManager)
{
notificationManager.CreateNotificationChannel(channel);

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, Constants.ClearCiphersCacheKey,
Constants.AndroidAllClearCipherCacheKeys);
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)
@@ -139,10 +147,12 @@ namespace Bit.Droid
var stateMigrationService =
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
var clipboardService = new ClipboardService(stateService);
var deviceActionService = new DeviceActionService(clipboardService, stateService, messagingService,
broadcasterService, () => ServiceContainer.Resolve<IEventService>("eventService"));
var deviceActionService = new DeviceActionService(stateService, messagingService);
var fileService = new FileService(stateService, broadcasterService);
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
messagingService, broadcasterService);
var autofillHandler = new AutofillHandler(stateService, messagingService, clipboardService,
platformUtilsService, new LazyResolve<IEventService>());
var biometricService = new BiometricService();
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
var cryptoService = new CryptoService(stateService, cryptoFunctionService);
@@ -159,6 +169,8 @@ namespace Bit.Droid
ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService);
ServiceContainer.Register<IClipboardService>("clipboardService", clipboardService);
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
ServiceContainer.Register<IFileService>(fileService);
ServiceContainer.Register<IAutofillHandler>(autofillHandler);
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.10.0" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="32" />
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2023.1.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
@@ -40,10 +40,16 @@
</intent-filter>
</activity>
</application>
<!-- Package visibility (for Android 11+) -->
<!-- Support for Xamarin.Essentials.Browser.OpenAsync (for Android > 11) -->
<!-- Related docs: https://learn.microsoft.com/en-us/xamarin/essentials/open-browser?tabs=android -->
<queries>
<intent>
<action android:name="*" />
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https"/>
</intent>
</queries>
</manifest>

View File

@@ -1,4 +1,5 @@
using Android.Content;
using Android.OS;
namespace Bit.Droid.Receivers
{
@@ -8,7 +9,17 @@ namespace Bit.Droid.Receivers
public override void OnReceive(Context context, Intent intent)
{
var clipboardManager = context.GetSystemService(Context.ClipboardService) as ClipboardManager;
clipboardManager.PrimaryClip = ClipData.NewPlainText("bitwarden", " ");
if (clipboardManager == null)
{
return;
}
// ClearPrimaryClip is supported down to API 28 with mixed results, so we're requiring 33+ instead
if ((int)Build.VERSION.SdkInt < 33)
{
clipboardManager.PrimaryClip = ClipData.NewPlainText("bitwarden", " ");
return;
}
clipboardManager.ClearPrimaryClip();
}
}
}

View File

@@ -0,0 +1,41 @@
using Android.Content;
using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.App.Services;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using CoreConstants = Bit.Core.Constants;
namespace Bit.Droid.Receivers
{
[BroadcastReceiver(Name = Constants.PACKAGE_NAME + "." + nameof(NotificationDismissReceiver), Exported = false)]
public class NotificationDismissReceiver : BroadcastReceiver
{
private readonly LazyResolve<IPushNotificationListenerService> _pushNotificationListenerService = new LazyResolve<IPushNotificationListenerService>();
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
public override void OnReceive(Context context, Intent intent)
{
try
{
if (intent?.GetStringExtra(CoreConstants.NotificationData) is string notificationDataJson)
{
var notificationType = JToken.Parse(notificationDataJson).SelectToken(CoreConstants.NotificationDataType);
if (notificationType.ToString() == PasswordlessNotificationData.TYPE)
{
_pushNotificationListenerService.Value.OnNotificationDismissed(JsonConvert.DeserializeObject<PasswordlessNotificationData>(notificationDataJson)).FireAndForget();
}
}
}
catch (System.Exception ex)
{
_logger.Value.Exception(ex);
}
}
}
}

View File

@@ -44,6 +44,7 @@ namespace Bit.Droid.Renderers
}
if (Control != null)
{
Control.SetHintTextColor(ThemeHelpers.MutedColor);
var t = ResourcesCompat.GetDrawable(Resources, Resource.Drawable.switch_thumb, null);
if (t is GradientDrawable thumb)
{

View File

@@ -17,6 +17,9 @@
<compatibility-package
android:name="alook.browser.google"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="app.vanadium.browser"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.amazon.cloud9"
android:maxLongVersionCode="10000000000"/>
@@ -89,6 +92,9 @@
<compatibility-package
android:name="com.kiwibrowser.browser.dev"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.lemurbrowser.exts"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.microsoft.emmx"
android:maxLongVersionCode="10000000000"/>
@@ -113,6 +119,9 @@
<compatibility-package
android:name="com.naver.whale"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.neeva.app"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.opera.browser"
android:maxLongVersionCode="10000000000"/>
@@ -137,6 +146,9 @@
<compatibility-package
android:name="com.qwant.liberty"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.rainsee.create"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.sec.android.app.sbrowser"
android:maxLongVersionCode="10000000000"/>
@@ -161,6 +173,15 @@
<compatibility-package
android:name="com.yandex.browser"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.yjllq.internet"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.yjllq.kito"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.yujian.ResideMenuDemo"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.z28j.feel"
android:maxLongVersionCode="10000000000"/>
@@ -182,6 +203,9 @@
<compatibility-package
android:name="mark.via.gp"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="net.dezor.browser"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="net.slions.fulguris.full.download"
android:maxLongVersionCode="10000000000"/>

View File

@@ -1,15 +1,21 @@
#if !FDROID
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.OS;
using AndroidX.Core.App;
using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Droid.Receivers;
using Bit.Droid.Utilities;
using Newtonsoft.Json;
using Xamarin.Forms;
using static Xamarin.Essentials.Platform;
using Intent = Android.Content.Intent;
namespace Bit.Droid.Services
{
@@ -67,28 +73,39 @@ namespace Bit.Droid.Services
}
}
public void SendLocalNotification(string title, string message, string notificationId)
public void SendLocalNotification(string title, string message, BaseNotificationData data)
{
if (string.IsNullOrEmpty(notificationId))
if (string.IsNullOrEmpty(data.Id))
{
throw new ArgumentNullException("notificationId cannot be null or empty.");
}
var context = Android.App.Application.Context;
var intent = new Intent(context, typeof(MainActivity));
intent.PutExtra(Bit.Core.Constants.NotificationData, JsonConvert.SerializeObject(data));
var pendingIntentFlags = AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true);
var pendingIntent = PendingIntent.GetActivity(context, 20220801, intent, pendingIntentFlags);
var builder = new NotificationCompat.Builder(context, Constants.AndroidNotificationChannelId)
var deleteIntent = new Intent(context, typeof(NotificationDismissReceiver));
deleteIntent.PutExtra(Bit.Core.Constants.NotificationData, JsonConvert.SerializeObject(data));
var deletePendingIntent = PendingIntent.GetBroadcast(context, 20220802, deleteIntent, pendingIntentFlags);
var builder = new NotificationCompat.Builder(context, Bit.Core.Constants.AndroidNotificationChannelId)
.SetContentIntent(pendingIntent)
.SetContentTitle(title)
.SetContentText(message)
.SetTimeoutAfter(Constants.PasswordlessNotificationTimeoutInMinutes * 60000)
.SetSmallIcon(Resource.Drawable.ic_notification)
.SetColor((int)Android.Graphics.Color.White)
.SetDeleteIntent(deletePendingIntent)
.SetAutoCancel(true);
if (data is PasswordlessNotificationData passwordlessNotificationData && passwordlessNotificationData.TimeoutInMinutes > 0)
{
builder.SetTimeoutAfter(passwordlessNotificationData.TimeoutInMinutes * 60000);
}
var notificationManager = NotificationManagerCompat.From(context);
notificationManager.Notify(int.Parse(notificationId), builder.Build());
notificationManager.Notify(int.Parse(data.Id), builder.Build());
}
}
}

View File

@@ -0,0 +1,215 @@
using System.Linq;
using System.Threading.Tasks;
using Android.App;
using Android.App.Assist;
using Android.Content;
using Android.OS;
using Android.Provider;
using Android.Views.Autofill;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Bit.Droid.Autofill;
using Plugin.CurrentActivity;
namespace Bit.Droid.Services
{
public class AutofillHandler : IAutofillHandler
{
private readonly IStateService _stateService;
private readonly IMessagingService _messagingService;
private readonly IClipboardService _clipboardService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly LazyResolve<IEventService> _eventService;
public AutofillHandler(IStateService stateService,
IMessagingService messagingService,
IClipboardService clipboardService,
IPlatformUtilsService platformUtilsService,
LazyResolve<IEventService> eventService)
{
_stateService = stateService;
_messagingService = messagingService;
_clipboardService = clipboardService;
_platformUtilsService = platformUtilsService;
_eventService = eventService;
}
public bool AutofillServiceEnabled()
{
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
{
return false;
}
try
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var afm = (AutofillManager)activity.GetSystemService(
Java.Lang.Class.FromType(typeof(AutofillManager)));
return afm.IsEnabled && afm.HasEnabledAutofillServices;
}
catch
{
return false;
}
}
public bool SupportsAutofillService()
{
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
{
return false;
}
try
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var type = Java.Lang.Class.FromType(typeof(AutofillManager));
var manager = activity.GetSystemService(type) as AutofillManager;
return manager.IsAutofillSupported;
}
catch
{
return false;
}
}
public void Autofill(CipherView cipher)
{
var activity = CrossCurrentActivity.Current.Activity as Xamarin.Forms.Platform.Android.FormsAppCompatActivity;
if (activity == null)
{
return;
}
if (activity.Intent?.GetBooleanExtra(AutofillConstants.AutofillFramework, false) ?? false)
{
if (cipher == null)
{
activity.SetResult(Result.Canceled);
activity.Finish();
return;
}
var structure = activity.Intent.GetParcelableExtra(
AutofillManager.ExtraAssistStructure) as AssistStructure;
if (structure == null)
{
activity.SetResult(Result.Canceled);
activity.Finish();
return;
}
var parser = new Parser(structure, activity.ApplicationContext);
parser.Parse();
if ((!parser.FieldCollection?.Fields?.Any() ?? true) || string.IsNullOrWhiteSpace(parser.Uri))
{
activity.SetResult(Result.Canceled);
activity.Finish();
return;
}
var task = CopyTotpAsync(cipher);
var dataset = AutofillHelpers.BuildDataset(activity, parser.FieldCollection, new FilledItem(cipher), false);
var replyIntent = new Intent();
replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, dataset);
activity.SetResult(Result.Ok, replyIntent);
activity.Finish();
var eventTask = _eventService.Value.CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
}
else
{
var data = new Intent();
if (cipher?.Login == null)
{
data.PutExtra("canceled", "true");
}
else
{
var task = CopyTotpAsync(cipher);
data.PutExtra("uri", cipher.Login.Uri);
data.PutExtra("username", cipher.Login.Username);
data.PutExtra("password", cipher.Login.Password);
}
if (activity.Parent == null)
{
activity.SetResult(Result.Ok, data);
}
else
{
activity.Parent.SetResult(Result.Ok, data);
}
activity.Finish();
_messagingService.Send("finishMainActivity");
if (cipher != null)
{
var eventTask = _eventService.Value.CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
}
}
}
public void CloseAutofill()
{
Autofill(null);
}
public bool AutofillAccessibilityServiceRunning()
{
var enabledServices = Settings.Secure.GetString(Application.Context.ContentResolver,
Settings.Secure.EnabledAccessibilityServices);
return Application.Context.PackageName != null &&
(enabledServices?.Contains(Application.Context.PackageName) ?? false);
}
public bool AutofillAccessibilityOverlayPermitted()
{
return Accessibility.AccessibilityHelpers.OverlayPermitted();
}
public void DisableAutofillService()
{
try
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var type = Java.Lang.Class.FromType(typeof(AutofillManager));
var manager = activity.GetSystemService(type) as AutofillManager;
manager.DisableAutofillServices();
}
catch { }
}
public bool AutofillServicesEnabled()
{
if (Build.VERSION.SdkInt <= BuildVersionCodes.M)
{
// Android 5-6: Both accessibility & overlay are required or nothing happens
return AutofillAccessibilityServiceRunning() && AutofillAccessibilityOverlayPermitted();
}
if (Build.VERSION.SdkInt == BuildVersionCodes.N)
{
// Android 7: Only accessibility is required (overlay is optional when using quick-action tile)
return AutofillAccessibilityServiceRunning();
}
// Android 8+: Either autofill or accessibility is required
return AutofillServiceEnabled() || AutofillAccessibilityServiceRunning();
}
private async Task CopyTotpAsync(CipherView cipher)
{
if (!string.IsNullOrWhiteSpace(cipher?.Login?.Totp))
{
var autoCopyDisabled = await _stateService.GetDisableAutoTotpCopyAsync();
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
if ((canAccessPremium || cipher.OrganizationUseTotp) && !autoCopyDisabled.GetValueOrDefault())
{
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
if (totp != null)
{
await _clipboardService.CopyTextAsync(totp);
_platformUtilsService.ShowToastForCopiedValue(AppResources.VerificationCodeTotp);
}
}
}
}
}
}

View File

@@ -6,7 +6,6 @@ using Android.OS;
using Bit.Core.Abstractions;
using Bit.Droid.Receivers;
using Bit.Droid.Utilities;
using Plugin.CurrentActivity;
using Xamarin.Essentials;
namespace Bit.Droid.Services
@@ -21,9 +20,9 @@ namespace Bit.Droid.Services
_stateService = stateService;
_clearClipboardPendingIntent = new Lazy<PendingIntent>(() =>
PendingIntent.GetBroadcast(CrossCurrentActivity.Current.Activity,
PendingIntent.GetBroadcast(Application.Context,
0,
new Intent(CrossCurrentActivity.Current.Activity, typeof(ClearClipboardAlarmReceiver)),
new Intent(Application.Context, typeof(ClearClipboardAlarmReceiver)),
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, false)));
}
@@ -45,7 +44,7 @@ namespace Bit.Droid.Services
}
catch (Java.Lang.SecurityException ex) when (ex.Message.Contains("does not belong to"))
{
// #1962 Just ignore, the content is copied either way but there is some app interfiering in the process
// #1962 Just ignore, the content is copied either way but there is some app interfering in the process
// that the OS catches and just throws this exception.
}
}
@@ -58,9 +57,7 @@ namespace Bit.Droid.Services
private void CopyToClipboard(string text, bool isSensitive = true)
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var clipboardManager = activity.GetSystemService(
Context.ClipboardService) as Android.Content.ClipboardManager;
var clipboardManager = Application.Context.GetSystemService(Context.ClipboardService) as ClipboardManager;
var clipData = ClipData.NewPlainText("bitwarden", text);
if (isSensitive)
{
@@ -87,7 +84,7 @@ namespace Bit.Droid.Services
return;
}
var triggerMs = Java.Lang.JavaSystem.CurrentTimeMillis() + clearMs;
var alarmManager = CrossCurrentActivity.Current.Activity.GetSystemService(Context.AlarmService) as AlarmManager;
var alarmManager = Application.Context.GetSystemService(Context.AlarmService) as AlarmManager;
alarmManager.Set(AlarmType.Rtc, triggerMs, _clearClipboardPendingIntent.Value);
}
}

View File

@@ -5,6 +5,8 @@ using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using System;
using System.Runtime.InteropServices;
using Java.Lang;
namespace Bit.Droid.Services
{
@@ -33,5 +35,19 @@ namespace Bit.Droid.Services
generator.Init(password, salt, iterations);
return ((KeyParameter)generator.GenerateDerivedMacParameters(keySize)).GetKey();
}
public byte[] Argon2id(byte[] password, byte[] salt, int iterations, int memory, int parallelism)
{
JavaSystem.LoadLibrary("argon2");
int keySize = 32;
var key = new byte[keySize];
argon2id_hash_raw(iterations, memory, parallelism,
password, password.Length, salt, salt.Length, key, key.Length);
return key;
}
[DllImport("argon2", EntryPoint = "argon2id_hash_raw")]
private static extern int argon2id_hash_raw(int timeCost, int memoryCost, int parallelism,
byte[] pwd, int pwdlen, byte[] salt, int saltlen, byte[] hash, int hashlen);
}
}

View File

@@ -1,11 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Android;
using Android.App;
using Android.App.Assist;
using Android.Content;
using Android.Content.PM;
using Android.Nfc;
@@ -14,59 +9,35 @@ using Android.Provider;
using Android.Text;
using Android.Text.Method;
using Android.Views;
using Android.Views.Autofill;
using Android.Views.InputMethods;
using Android.Webkit;
using Android.Widget;
using AndroidX.Core.App;
using AndroidX.Core.Content;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Bit.Droid.Autofill;
using Bit.Droid.Utilities;
using Plugin.CurrentActivity;
using static Bit.App.Pages.SettingsPageViewModel;
namespace Bit.Droid.Services
{
public class DeviceActionService : IDeviceActionService
{
private readonly IClipboardService _clipboardService;
private readonly IStateService _stateService;
private readonly IMessagingService _messagingService;
private readonly IBroadcasterService _broadcasterService;
private readonly Func<IEventService> _eventServiceFunc;
private AlertDialog _progressDialog;
object _progressDialogLock = new object();
private bool _cameraPermissionsDenied;
private Toast _toast;
private string _userAgent;
public DeviceActionService(
IClipboardService clipboardService,
IStateService stateService,
IMessagingService messagingService,
IBroadcasterService broadcasterService,
Func<IEventService> eventServiceFunc)
IMessagingService messagingService)
{
_clipboardService = clipboardService;
_stateService = stateService;
_messagingService = messagingService;
_broadcasterService = broadcasterService;
_eventServiceFunc = eventServiceFunc;
_broadcasterService.Subscribe(nameof(DeviceActionService), (message) =>
{
if (message.Command == "selectFileCameraPermissionDenied")
{
_cameraPermissionsDenied = true;
}
});
}
public string DeviceUserAgent
@@ -99,14 +70,17 @@ namespace Bit.Droid.Services
public bool LaunchApp(string appName)
{
if ((int)Build.VERSION.SdkInt < 33)
{
// API 33 required to avoid using wildcard app visibility or dangerous permissions
// https://developer.android.com/reference/android/content/pm/PackageManager#getLaunchIntentSenderForPackage(java.lang.String)
return false;
}
var activity = CrossCurrentActivity.Current.Activity;
appName = appName.Replace("androidapp://", string.Empty);
var launchIntent = activity.PackageManager.GetLaunchIntentForPackage(appName);
if (launchIntent != null)
{
activity.StartActivity(launchIntent);
}
return launchIntent != null;
var launchIntentSender = activity?.PackageManager?.GetLaunchIntentSenderForPackage(appName);
launchIntentSender?.SendIntent(activity, Result.Ok, null, null, null);
return launchIntentSender != null;
}
public async Task ShowLoadingAsync(string text)
@@ -212,184 +186,6 @@ namespace Bit.Droid.Services
return true;
}
public bool OpenFile(byte[] fileData, string id, string fileName)
{
try
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var intent = BuildOpenFileIntent(fileData, fileName);
if (intent == null)
{
return false;
}
activity.StartActivity(intent);
return true;
}
catch { }
return false;
}
public bool CanOpenFile(string fileName)
{
try
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var intent = BuildOpenFileIntent(new byte[0], string.Concat("opentest_", fileName));
if (intent == null)
{
return false;
}
var activities = activity.PackageManager.QueryIntentActivities(intent,
PackageInfoFlags.MatchDefaultOnly);
return (activities?.Count ?? 0) > 0;
}
catch { }
return false;
}
private Intent BuildOpenFileIntent(byte[] fileData, string fileName)
{
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
if (extension == null)
{
return null;
}
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
if (mimeType == null)
{
return null;
}
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var cachePath = activity.CacheDir;
var filePath = Path.Combine(cachePath.Path, fileName);
File.WriteAllBytes(filePath, fileData);
var file = new Java.IO.File(cachePath, fileName);
if (!file.IsFile)
{
return null;
}
try
{
var intent = new Intent(Intent.ActionView);
var uri = FileProvider.GetUriForFile(activity.ApplicationContext,
"com.x8bit.bitwarden.fileprovider", file);
intent.SetDataAndType(uri, mimeType);
intent.SetFlags(ActivityFlags.GrantReadUriPermission);
return intent;
}
catch { }
return null;
}
public bool SaveFile(byte[] fileData, string id, string fileName, string contentUri)
{
try
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
if (contentUri != null)
{
var uri = Android.Net.Uri.Parse(contentUri);
var stream = activity.ContentResolver.OpenOutputStream(uri);
// Using java bufferedOutputStream due to this issue:
// https://github.com/xamarin/xamarin-android/issues/3498
var javaStream = new Java.IO.BufferedOutputStream(stream);
javaStream.Write(fileData);
javaStream.Flush();
javaStream.Close();
return true;
}
// Prompt for location to save file
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
if (extension == null)
{
return false;
}
string mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
if (mimeType == null)
{
// Unable to identify so fall back to generic "any" type
mimeType = "*/*";
}
var intent = new Intent(Intent.ActionCreateDocument);
intent.SetType(mimeType);
intent.AddCategory(Intent.CategoryOpenable);
intent.PutExtra(Intent.ExtraTitle, fileName);
activity.StartActivityForResult(intent, Constants.SaveFileRequestCode);
return true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", ex.GetType(), ex.StackTrace);
}
return false;
}
public async Task ClearCacheAsync()
{
try
{
DeleteDir(CrossCurrentActivity.Current.Activity.CacheDir);
await _stateService.SetLastFileCacheClearAsync(DateTime.UtcNow);
}
catch (Exception) { }
}
public Task SelectFileAsync()
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var hasStorageWritePermission = !_cameraPermissionsDenied &&
HasPermission(Manifest.Permission.WriteExternalStorage);
var additionalIntents = new List<IParcelable>();
if (activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera))
{
var hasCameraPermission = !_cameraPermissionsDenied && HasPermission(Manifest.Permission.Camera);
if (!_cameraPermissionsDenied && !hasStorageWritePermission)
{
AskPermission(Manifest.Permission.WriteExternalStorage);
return Task.FromResult(0);
}
if (!_cameraPermissionsDenied && !hasCameraPermission)
{
AskPermission(Manifest.Permission.Camera);
return Task.FromResult(0);
}
if (!_cameraPermissionsDenied && hasCameraPermission && hasStorageWritePermission)
{
try
{
var file = new Java.IO.File(activity.FilesDir, "temp_camera_photo.jpg");
if (!file.Exists())
{
file.ParentFile.Mkdirs();
file.CreateNewFile();
}
var outputFileUri = FileProvider.GetUriForFile(activity,
"com.x8bit.bitwarden.fileprovider", file);
additionalIntents.AddRange(GetCameraIntents(outputFileUri));
}
catch (Java.IO.IOException) { }
}
}
var docIntent = new Intent(Intent.ActionOpenDocument);
docIntent.AddCategory(Intent.CategoryOpenable);
docIntent.SetType("*/*");
var chooserIntent = Intent.CreateChooser(docIntent, AppResources.FileSource);
if (additionalIntents.Count > 0)
{
chooserIntent.PutExtra(Intent.ExtraInitialIntents, additionalIntents.ToArray());
}
activity.StartActivityForResult(chooserIntent, Constants.SelectFileRequestCode);
return Task.FromResult(0);
}
public Task<string> DisplayPromptAync(string title = null, string description = null,
string text = null, string okButtonText = null, string cancelButtonText = null,
bool numericKeyboard = false, bool autofocus = true, bool password = false)
@@ -467,34 +263,6 @@ namespace Bit.Droid.Services
}
}
public void DisableAutofillService()
{
try
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var type = Java.Lang.Class.FromType(typeof(AutofillManager));
var manager = activity.GetSystemService(type) as AutofillManager;
manager.DisableAutofillServices();
}
catch { }
}
public bool AutofillServicesEnabled()
{
if (Build.VERSION.SdkInt <= BuildVersionCodes.M)
{
// Android 5-6: Both accessibility & overlay are required or nothing happens
return AutofillAccessibilityServiceRunning() && AutofillAccessibilityOverlayPermitted();
}
if (Build.VERSION.SdkInt == BuildVersionCodes.N)
{
// Android 7: Only accessibility is required (overlay is optional when using quick-action tile)
return AutofillAccessibilityServiceRunning();
}
// Android 8+: Either autofill or accessibility is required
return AutofillServiceEnabled() || AutofillAccessibilityServiceRunning();
}
public string GetBuildNumber()
{
return Application.Context.ApplicationContext.PackageManager.GetPackageInfo(
@@ -526,25 +294,6 @@ namespace Bit.Droid.Services
return activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera);
}
public bool SupportsAutofillService()
{
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
{
return false;
}
try
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var type = Java.Lang.Class.FromType(typeof(AutofillManager));
var manager = activity.GetSystemService(type) as AutofillManager;
return manager.IsAutofillSupported;
}
catch
{
return false;
}
}
public int SystemMajorVersion()
{
return (int)Build.VERSION.SdkInt;
@@ -635,112 +384,6 @@ namespace Bit.Droid.Services
title, cancel, destruction, buttons);
}
public void Autofill(CipherView cipher)
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
if (activity == null)
{
return;
}
if (activity.Intent?.GetBooleanExtra("autofillFramework", false) ?? false)
{
if (cipher == null)
{
activity.SetResult(Result.Canceled);
activity.Finish();
return;
}
var structure = activity.Intent.GetParcelableExtra(
AutofillManager.ExtraAssistStructure) as AssistStructure;
if (structure == null)
{
activity.SetResult(Result.Canceled);
activity.Finish();
return;
}
var parser = new Parser(structure, activity.ApplicationContext);
parser.Parse();
if ((!parser.FieldCollection?.Fields?.Any() ?? true) || string.IsNullOrWhiteSpace(parser.Uri))
{
activity.SetResult(Result.Canceled);
activity.Finish();
return;
}
var task = CopyTotpAsync(cipher);
var dataset = AutofillHelpers.BuildDataset(activity, parser.FieldCollection, new FilledItem(cipher));
var replyIntent = new Intent();
replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, dataset);
activity.SetResult(Result.Ok, replyIntent);
activity.Finish();
var eventTask = _eventServiceFunc().CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
}
else
{
var data = new Intent();
if (cipher?.Login == null)
{
data.PutExtra("canceled", "true");
}
else
{
var task = CopyTotpAsync(cipher);
data.PutExtra("uri", cipher.Login.Uri);
data.PutExtra("username", cipher.Login.Username);
data.PutExtra("password", cipher.Login.Password);
}
if (activity.Parent == null)
{
activity.SetResult(Result.Ok, data);
}
else
{
activity.Parent.SetResult(Result.Ok, data);
}
activity.Finish();
_messagingService.Send("finishMainActivity");
if (cipher != null)
{
var eventTask = _eventServiceFunc().CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
}
}
}
public void CloseAutofill()
{
Autofill(null);
}
public void Background()
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
if (activity.Intent?.GetBooleanExtra("autofillFramework", false) ?? false)
{
activity.SetResult(Result.Canceled);
activity.Finish();
}
else
{
activity.MoveTaskToBack(true);
}
}
public bool AutofillAccessibilityServiceRunning()
{
var enabledServices = Settings.Secure.GetString(Application.Context.ContentResolver,
Settings.Secure.EnabledAccessibilityServices);
return Application.Context.PackageName != null &&
(enabledServices?.Contains(Application.Context.PackageName) ?? false);
}
public bool AutofillAccessibilityOverlayPermitted()
{
return Accessibility.AccessibilityHelpers.OverlayPermitted();
}
public bool HasAutofillService()
{
return true;
}
public void OpenAccessibilityOverlayPermissionSettings()
{
@@ -771,25 +414,6 @@ namespace Bit.Droid.Services
}
}
public bool AutofillServiceEnabled()
{
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
{
return false;
}
try
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var afm = (AutofillManager)activity.GetSystemService(
Java.Lang.Class.FromType(typeof(AutofillManager)));
return afm.IsEnabled && afm.HasEnabledAutofillServices;
}
catch
{
return false;
}
}
public void OpenAccessibilitySettings()
{
try
@@ -848,61 +472,6 @@ namespace Bit.Droid.Services
return true;
}
private bool DeleteDir(Java.IO.File dir)
{
if (dir != null && dir.IsDirectory)
{
var children = dir.List();
for (int i = 0; i < children.Length; i++)
{
var success = DeleteDir(new Java.IO.File(dir, children[i]));
if (!success)
{
return false;
}
}
return dir.Delete();
}
else if (dir != null && dir.IsFile)
{
return dir.Delete();
}
else
{
return false;
}
}
private bool HasPermission(string permission)
{
return ContextCompat.CheckSelfPermission(
CrossCurrentActivity.Current.Activity, permission) == Permission.Granted;
}
private void AskPermission(string permission)
{
ActivityCompat.RequestPermissions(CrossCurrentActivity.Current.Activity, new string[] { permission },
Constants.SelectFilePermissionRequestCode);
}
private List<IParcelable> GetCameraIntents(Android.Net.Uri outputUri)
{
var intents = new List<IParcelable>();
var pm = CrossCurrentActivity.Current.Activity.PackageManager;
var captureIntent = new Intent(MediaStore.ActionImageCapture);
var listCam = pm.QueryIntentActivities(captureIntent, 0);
foreach (var res in listCam)
{
var packageName = res.ActivityInfo.PackageName;
var intent = new Intent(captureIntent);
intent.SetComponent(new ComponentName(packageName, res.ActivityInfo.Name));
intent.SetPackage(packageName);
intent.PutExtra(MediaStore.ExtraOutput, outputUri);
intents.Add(intent);
}
return intents;
}
private Intent RateIntentForUrl(string url, Activity activity)
{
var intent = new Intent(Intent.ActionView, Android.Net.Uri.Parse($"{url}?id={activity.PackageName}"));
@@ -920,24 +489,6 @@ namespace Bit.Droid.Services
return intent;
}
private async Task CopyTotpAsync(CipherView cipher)
{
if (!string.IsNullOrWhiteSpace(cipher?.Login?.Totp))
{
var autoCopyDisabled = await _stateService.GetDisableAutoTotpCopyAsync();
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
if ((canAccessPremium || cipher.OrganizationUseTotp) && !autoCopyDisabled.GetValueOrDefault())
{
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
if (totp != null)
{
await _clipboardService.CopyTextAsync(totp);
}
}
}
}
public float GetSystemFontSizeScale()
{
var activity = CrossCurrentActivity.Current?.Activity as MainActivity;
@@ -973,5 +524,11 @@ namespace Bit.Droid.Services
intent.SetData(uri);
Application.Context.StartActivity(intent);
}
public void CloseExtensionPopUp()
{
// only used by iOS
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,278 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Android;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using Android.Provider;
using Android.Webkit;
using AndroidX.Core.App;
using AndroidX.Core.Content;
using Bit.App.Resources;
using Bit.Core;
using Bit.Core.Abstractions;
using Plugin.CurrentActivity;
namespace Bit.Droid.Services
{
public class FileService : IFileService
{
private readonly IStateService _stateService;
private readonly IBroadcasterService _broadcasterService;
private bool _cameraPermissionsDenied;
public FileService(IStateService stateService, IBroadcasterService broadcasterService)
{
_stateService = stateService;
_broadcasterService = broadcasterService;
_broadcasterService.Subscribe(nameof(FileService), (message) =>
{
if (message.Command == "selectFileCameraPermissionDenied")
{
_cameraPermissionsDenied = true;
}
});
}
public bool OpenFile(byte[] fileData, string id, string fileName)
{
try
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var intent = BuildOpenFileIntent(fileData, fileName);
if (intent == null)
{
return false;
}
activity.StartActivity(intent);
return true;
}
catch { }
return false;
}
public bool CanOpenFile(string fileName)
{
try
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var intent = BuildOpenFileIntent(new byte[0], string.Concat("opentest_", fileName));
if (intent == null)
{
return false;
}
var activities = activity.PackageManager.QueryIntentActivities(intent,
PackageInfoFlags.MatchDefaultOnly);
return (activities?.Count ?? 0) > 0;
}
catch { }
return false;
}
private Intent BuildOpenFileIntent(byte[] fileData, string fileName)
{
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
if (extension == null)
{
return null;
}
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
if (mimeType == null)
{
return null;
}
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var cachePath = activity.CacheDir;
var filePath = Path.Combine(cachePath.Path, fileName);
File.WriteAllBytes(filePath, fileData);
var file = new Java.IO.File(cachePath, fileName);
if (!file.IsFile)
{
return null;
}
try
{
var intent = new Intent(Intent.ActionView);
var uri = FileProvider.GetUriForFile(activity.ApplicationContext,
"com.x8bit.bitwarden.fileprovider", file);
intent.SetDataAndType(uri, mimeType);
intent.SetFlags(ActivityFlags.GrantReadUriPermission);
return intent;
}
catch { }
return null;
}
public bool SaveFile(byte[] fileData, string id, string fileName, string contentUri)
{
try
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
if (contentUri != null)
{
var uri = Android.Net.Uri.Parse(contentUri);
var stream = activity.ContentResolver.OpenOutputStream(uri);
// Using java bufferedOutputStream due to this issue:
// https://github.com/xamarin/xamarin-android/issues/3498
var javaStream = new Java.IO.BufferedOutputStream(stream);
javaStream.Write(fileData);
javaStream.Flush();
javaStream.Close();
return true;
}
// Prompt for location to save file
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
if (extension == null)
{
return false;
}
string mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
if (mimeType == null)
{
// Unable to identify so fall back to generic "any" type
mimeType = "*/*";
}
var intent = new Intent(Intent.ActionCreateDocument);
intent.SetType(mimeType);
intent.AddCategory(Intent.CategoryOpenable);
intent.PutExtra(Intent.ExtraTitle, fileName);
activity.StartActivityForResult(intent, Core.Constants.SaveFileRequestCode);
return true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", ex.GetType(), ex.StackTrace);
}
return false;
}
public async Task ClearCacheAsync()
{
try
{
DeleteDir(CrossCurrentActivity.Current.Activity.CacheDir);
await _stateService.SetLastFileCacheClearAsync(DateTime.UtcNow);
}
catch (Exception) { }
}
public Task SelectFileAsync()
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var hasStorageWritePermission = !_cameraPermissionsDenied &&
HasPermission(Manifest.Permission.WriteExternalStorage);
var additionalIntents = new List<IParcelable>();
if (activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera))
{
var hasCameraPermission = !_cameraPermissionsDenied && HasPermission(Manifest.Permission.Camera);
if (!_cameraPermissionsDenied && !hasStorageWritePermission)
{
AskPermission(Manifest.Permission.WriteExternalStorage);
return Task.FromResult(0);
}
if (!_cameraPermissionsDenied && !hasCameraPermission)
{
AskPermission(Manifest.Permission.Camera);
return Task.FromResult(0);
}
if (!_cameraPermissionsDenied && hasCameraPermission && hasStorageWritePermission)
{
try
{
var file = new Java.IO.File(activity.FilesDir, "temp_camera_photo.jpg");
if (!file.Exists())
{
file.ParentFile.Mkdirs();
file.CreateNewFile();
}
var outputFileUri = FileProvider.GetUriForFile(activity,
"com.x8bit.bitwarden.fileprovider", file);
additionalIntents.AddRange(GetCameraIntents(outputFileUri));
}
catch (Java.IO.IOException) { }
}
}
var docIntent = new Intent(Intent.ActionOpenDocument);
docIntent.AddCategory(Intent.CategoryOpenable);
docIntent.SetType("*/*");
var chooserIntent = Intent.CreateChooser(docIntent, AppResources.FileSource);
if (additionalIntents.Count > 0)
{
chooserIntent.PutExtra(Intent.ExtraInitialIntents, additionalIntents.ToArray());
}
activity.StartActivityForResult(chooserIntent, Core.Constants.SelectFileRequestCode);
return Task.FromResult(0);
}
private bool DeleteDir(Java.IO.File dir)
{
if (dir is null)
{
return false;
}
if (dir.IsDirectory)
{
var children = dir.List();
for (int i = 0; i < children.Length; i++)
{
var success = DeleteDir(new Java.IO.File(dir, children[i]));
if (!success)
{
return false;
}
}
return dir.Delete();
}
if (dir.IsFile)
{
return dir.Delete();
}
return false;
}
private bool HasPermission(string permission)
{
return ContextCompat.CheckSelfPermission(
CrossCurrentActivity.Current.Activity, permission) == Permission.Granted;
}
private void AskPermission(string permission)
{
ActivityCompat.RequestPermissions(CrossCurrentActivity.Current.Activity, new string[] { permission },
Core.Constants.SelectFilePermissionRequestCode);
}
private List<IParcelable> GetCameraIntents(Android.Net.Uri outputUri)
{
var intents = new List<IParcelable>();
var pm = CrossCurrentActivity.Current.Activity.PackageManager;
var captureIntent = new Intent(MediaStore.ActionImageCapture);
var listCam = pm.QueryIntentActivities(captureIntent, 0);
foreach (var res in listCam)
{
var packageName = res.ActivityInfo.PackageName;
var intent = new Intent(captureIntent);
intent.SetComponent(new ComponentName(packageName, res.ActivityInfo.Name));
intent.SetPackage(packageName);
intent.PutExtra(MediaStore.ExtraOutput, outputUri);
intents.Add(intent);
}
return intents;
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.Globalization;
using Bit.App.Abstractions;
using Bit.App.Models;
@@ -25,13 +26,13 @@ namespace Bit.Droid.Services
try
{
var fallback = ToDotnetFallbackLanguage(new PlatformCulture(netLanguage));
Console.WriteLine(netLanguage + " failed, trying " + fallback + " (" + e1.Message + ")");
Debug.WriteLine(netLanguage + " failed, trying " + fallback + " (" + e1.Message + ")");
ci = new CultureInfo(fallback);
}
catch (CultureNotFoundException e2)
{
// iOS language not valid .NET culture, falling back to English
Console.WriteLine(netLanguage + " couldn't be set, using 'en' (" + e2.Message + ")");
Debug.WriteLine(netLanguage + " couldn't be set, using 'en' (" + e2.Message + ")");
ci = new CultureInfo("en");
}
}
@@ -40,7 +41,7 @@ namespace Bit.Droid.Services
private string AndroidToDotnetLanguage(string androidLanguage)
{
Console.WriteLine("Android Language:" + androidLanguage);
Debug.WriteLine("Android Language:" + androidLanguage);
var netLanguage = androidLanguage;
if (androidLanguage.StartsWith("zh"))
{
@@ -79,13 +80,13 @@ namespace Bit.Droid.Services
// ONLY use cultures that have been tested and known to work
}
}
Console.WriteLine(".NET Language/Locale:" + netLanguage);
Debug.WriteLine(".NET Language/Locale:" + netLanguage);
return netLanguage;
}
private string ToDotnetFallbackLanguage(PlatformCulture platCulture)
{
Console.WriteLine(".NET Fallback Language:" + platCulture.LanguageCode);
Debug.WriteLine(".NET Fallback Language:" + platCulture.LanguageCode);
var netLanguage = platCulture.LanguageCode; // use the first part of the identifier (two chars, usually);
switch (platCulture.LanguageCode)
{
@@ -95,7 +96,7 @@ namespace Bit.Droid.Services
// add more application-specific cases here (if required)
// ONLY use cultures that have been tested and known to work
}
Console.WriteLine(".NET Fallback Language/Locale:" + netLanguage + " (application-specific)");
Debug.WriteLine(".NET Fallback Language/Locale:" + netLanguage + " (application-specific)");
return netLanguage;
}

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

@@ -62,7 +62,7 @@ namespace Bit.Droid.Utilities
theme = ThemeManager.Dark;
}
if (theme == ThemeManager.Dark || theme == ThemeManager.Black || theme == ThemeManager.Nord)
if (theme == ThemeManager.Dark || theme == ThemeManager.Black || theme == ThemeManager.Nord || theme == ThemeManager.SolarizedDark)
{
LightTheme = false;
}

Binary file not shown.

Binary file not shown.

BIN
src/Android/lib/x86/libargon2.so Executable file

Binary file not shown.

Binary file not shown.

View File

@@ -9,5 +9,6 @@ namespace Bit.App.Abstractions
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
Task LogOutAsync(string userId, bool userInitiated, bool expired);
Task PromptToSwitchToExistingAccountAsync(string userId);
}
}

View File

@@ -1,6 +1,6 @@
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Models.View;
using Bit.Core.Models;
namespace Bit.App.Abstractions
{
@@ -8,47 +8,36 @@ namespace Bit.App.Abstractions
{
string DeviceUserAgent { get; }
DeviceType DeviceType { get; }
int SystemMajorVersion();
string SystemModel();
string GetBuildNumber();
void Toast(string text, bool longDuration = false);
bool LaunchApp(string appName);
Task ShowLoadingAsync(string text);
Task HideLoadingAsync();
bool OpenFile(byte[] fileData, string id, string fileName);
bool SaveFile(byte[] fileData, string id, string fileName, string contentUri);
bool CanOpenFile(string fileName);
Task ClearCacheAsync();
Task SelectFileAsync();
Task<string> DisplayPromptAync(string title = null, string description = null, string text = null,
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
bool autofocus = true, bool password = false);
void RateApp();
Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons);
Task<string> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons);
bool SupportsFaceBiometric();
Task<bool> SupportsFaceBiometricAsync();
bool SupportsNfc();
bool SupportsCamera();
bool SupportsAutofillService();
int SystemMajorVersion();
string SystemModel();
Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons);
Task<string> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons);
void Autofill(CipherView cipher);
void CloseAutofill();
void Background();
bool AutofillAccessibilityServiceRunning();
bool AutofillAccessibilityOverlayPermitted();
bool HasAutofillService();
bool AutofillServiceEnabled();
void DisableAutofillService();
bool AutofillServicesEnabled();
string GetBuildNumber();
bool SupportsFido2();
bool LaunchApp(string appName);
void RateApp();
void OpenAccessibilitySettings();
void OpenAccessibilityOverlayPermissionSettings();
void OpenAutofillSettings();
long GetActiveTime();
void CloseMainApp();
bool SupportsFido2();
float GetSystemFontSizeScale();
Task OnAccountSwitchCompleteAsync();
Task SetScreenCaptureAllowedAsync();
void OpenAppSettings();
void CloseExtensionPopUp();
}
}

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using Bit.App.Models;
using Newtonsoft.Json.Linq;
namespace Bit.App.Abstractions
@@ -9,6 +10,8 @@ namespace Bit.App.Abstractions
Task OnRegisteredAsync(string token, string device);
void OnUnregistered(string device);
void OnError(string message, string device);
Task OnNotificationTapped(BaseNotificationData data);
Task OnNotificationDismissed(BaseNotificationData data);
bool ShouldShowNotification();
}
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Models;
namespace Bit.App.Abstractions
{
@@ -10,7 +11,7 @@ namespace Bit.App.Abstractions
Task<string> GetTokenAsync();
Task RegisterAsync();
Task UnregisterAsync();
void SendLocalNotification(string title, string message, string notificationId);
void SendLocalNotification(string title, string message, BaseNotificationData data);
void DismissLocalNotification(string notificationId);
}
}

View File

@@ -21,7 +21,6 @@
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2515" />
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
<PackageReference Include="Xamarin.CommunityToolkit" Version="1.3.0" />
</ItemGroup>
<ItemGroup>
@@ -126,6 +125,9 @@
<Compile Update="Pages\Accounts\LoginPasswordlessPage.xaml.cs">
<DependentUpon>LoginPasswordlessPage.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Accounts\LoginPasswordlessRequestPage.xaml.cs">
<DependentUpon>LoginPasswordlessRequestPage.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
@@ -140,6 +142,8 @@
<Folder Include="Controls\AccountSwitchingOverlay\" />
<Folder Include="Utilities\AccountManagement\" />
<Folder Include="Controls\DateTime\" />
<Folder Include="Controls\IconLabelButton\" />
<Folder Include="Controls\PasswordStrengthProgressBar\" />
</ItemGroup>
<ItemGroup>
@@ -431,5 +435,7 @@
<None Remove="Controls\AccountSwitchingOverlay\" />
<None Remove="Utilities\AccountManagement\" />
<None Remove="Controls\DateTime\" />
<None Remove="Controls\IconLabelButton\" />
<None Remove="Controls\PasswordStrengthProgressBar\" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models;
@@ -11,6 +12,7 @@ using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Response;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.Forms;
@@ -28,11 +30,13 @@ namespace Bit.App
private readonly ISyncService _syncService;
private readonly IAuthService _authService;
private readonly IDeviceActionService _deviceActionService;
private readonly IFileService _fileService;
private readonly IAccountsManager _accountsManager;
private readonly IPushNotificationService _pushNotificationService;
private static bool _isResumed;
// this variable is static because the app is launching new activities on notification click, creating new instances of App.
// these variables are static because the app is launching new activities on notification click, creating new instances of App.
private static bool _pendingCheckPasswordlessLoginRequests;
private static object _processingLoginRequestLock = new object();
public App(AppOptions appOptions)
{
@@ -49,6 +53,7 @@ namespace Bit.App
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
_authService = ServiceContainer.Resolve<IAuthService>("authService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_fileService = ServiceContainer.Resolve<IFileService>();
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
@@ -140,9 +145,15 @@ namespace Bit.App
new NavigationPage(new RemoveMasterPasswordPage()));
});
}
else if (message.Command == "passwordlessLoginRequest" || message.Command == "unlocked" || message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
else if (message.Command == Constants.PasswordlessLoginRequestKey
|| message.Command == "unlocked"
|| message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
{
CheckPasswordlessLoginRequestsAsync().FireAndForget();
lock (_processingLoginRequestLock)
{
// lock doesn't allow for async execution
CheckPasswordlessLoginRequestsAsync().Wait();
}
}
}
catch (Exception ex)
@@ -159,22 +170,30 @@ namespace Bit.App
_pendingCheckPasswordlessLoginRequests = true;
return;
}
_pendingCheckPasswordlessLoginRequests = false;
if (await _vaultTimeoutService.IsLockedAsync())
{
return;
}
var notification = await _stateService.GetPasswordlessLoginNotificationAsync();
if (notification == null)
{
return;
}
if (await CheckShouldSwitchActiveUserAsync(notification))
{
return;
}
// Delay to wait for the vault page to appear
await Task.Delay(2000);
// if there is a request modal opened ignore all incoming requests
if (App.Current.MainPage.Navigation.ModalStack.Any(p => p is NavigationPage navPage && navPage.CurrentPage is LoginPasswordlessPage))
{
return;
}
var loginRequestData = await _authService.GetPasswordlessLoginRequestByIdAsync(notification.Id);
var page = new LoginPasswordlessPage(new LoginPasswordlessDetails()
{
@@ -185,16 +204,44 @@ namespace Bit.App
FingerprintPhrase = loginRequestData.RequestFingerprint,
RequestDate = loginRequestData.CreationDate,
DeviceType = loginRequestData.RequestDeviceType,
Origin = loginRequestData.Origin,
Origin = loginRequestData.Origin
});
await _stateService.SetPasswordlessLoginNotificationAsync(null);
_pushNotificationService.DismissLocalNotification(Constants.PasswordlessNotificationId);
if (loginRequestData.CreationDate.AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) > DateTime.UtcNow)
if (!loginRequestData.IsExpired)
{
await Device.InvokeOnMainThreadAsync(() => Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page)));
}
}
private async Task<bool> CheckShouldSwitchActiveUserAsync(PasswordlessRequestNotification notification)
{
var activeUserId = await _stateService.GetActiveUserIdAsync();
if (notification.UserId == activeUserId)
{
return false;
}
var notificationUserEmail = await _stateService.GetEmailAsync(notification.UserId);
Device.BeginInvokeOnMainThread(async () =>
{
try
{
var result = await _deviceActionService.DisplayAlertAsync(AppResources.LogInRequested, string.Format(AppResources.LoginAttemptFromXDoYouWantToSwitchToThisAccount, notificationUserEmail), AppResources.Cancel, AppResources.Ok);
if (result == AppResources.Ok)
{
await _stateService.SetActiveUserAsync(notification.UserId);
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
}
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
});
return true;
}
public AppOptions Options { get; private set; }
protected async override void OnStart()
@@ -214,7 +261,7 @@ namespace Bit.App
}
if (_pendingCheckPasswordlessLoginRequests)
{
CheckPasswordlessLoginRequestsAsync().FireAndForget();
_messagingService.Send(Constants.PasswordlessLoginRequestKey);
}
if (Device.RuntimePlatform == Device.Android)
{
@@ -250,7 +297,7 @@ namespace Bit.App
_isResumed = true;
if (_pendingCheckPasswordlessLoginRequests)
{
CheckPasswordlessLoginRequestsAsync().FireAndForget();
_messagingService.Send(Constants.PasswordlessLoginRequestKey);
}
if (Device.RuntimePlatform == Device.Android)
{
@@ -301,7 +348,7 @@ namespace Bit.App
var lastClear = await _stateService.GetLastFileCacheClearAsync();
if ((DateTime.UtcNow - lastClear.GetValueOrDefault(DateTime.MinValue)).TotalDays >= 1)
{
var task = Task.Run(() => _deviceActionService.ClearCacheAsync());
var task = Task.Run(() => _fileService.ClearCacheAsync());
}
}

View File

@@ -14,7 +14,7 @@ namespace Bit.App.Controls
{
AccountView = accountView;
AvatarImageSource = ServiceContainer.Resolve<IAvatarImageSourcePool>("avatarImageSourcePool")
?.GetOrCreateAvatar(AccountView.Name, AccountView.Email);
?.GetOrCreateAvatar(AccountView.UserId, AccountView.Name, AccountView.Email, AccountView.AvatarColor);
}
public AccountView AccountView

View File

@@ -3,6 +3,7 @@ using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Bit.Core.Utilities;
using SkiaSharp;
using Xamarin.Forms;
@@ -10,7 +11,9 @@ namespace Bit.App.Controls
{
public class AvatarImageSource : StreamImageSource
{
private string _data;
private readonly string _text;
private readonly string _id;
private readonly string _color;
public override bool Equals(object obj)
{
@@ -21,21 +24,23 @@ namespace Bit.App.Controls
if (obj is AvatarImageSource avatar)
{
return avatar._data == _data;
return avatar._id == _id && avatar._text == _text && avatar._color == _color;
}
return base.Equals(obj);
}
public override int GetHashCode() => _data?.GetHashCode() ?? -1;
public override int GetHashCode() => _id?.GetHashCode() ?? _text?.GetHashCode() ?? -1;
public AvatarImageSource(string name = null, string email = null)
public AvatarImageSource(string userId = null, string name = null, string email = null, string color = null)
{
_data = name;
if (string.IsNullOrWhiteSpace(_data))
_id = userId;
_text = name;
if (string.IsNullOrWhiteSpace(_text))
{
_data = email;
_text = email;
}
_color = color;
}
public override Func<CancellationToken, Task<Stream>> Stream => GetStreamAsync;
@@ -52,24 +57,24 @@ namespace Bit.App.Controls
private Stream Draw()
{
string chars;
string upperData = null;
string upperCaseText = null;
if (string.IsNullOrEmpty(_data))
if (string.IsNullOrEmpty(_text))
{
chars = "..";
}
else if (_data?.Length > 1)
else if (_text?.Length > 1)
{
upperData = _data.ToUpper();
chars = GetFirstLetters(upperData, 2);
upperCaseText = _text.ToUpper();
chars = GetFirstLetters(upperCaseText, 2);
}
else
{
chars = upperData = _data.ToUpper();
chars = upperCaseText = _text.ToUpper();
}
var bgColor = StringToColor(upperData);
var textColor = Color.White;
var bgColor = _color ?? CoreHelpers.StringToColor(_id ?? upperCaseText, "#33ffffff");
var textColor = CoreHelpers.TextColorFromBgColor(bgColor);
var size = 50;
using (var bitmap = new SKBitmap(size * 2,
@@ -85,7 +90,7 @@ namespace Bit.App.Controls
IsAntialias = true,
Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor.ToHex())
Color = SKColor.Parse(bgColor)
})
{
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
@@ -97,7 +102,7 @@ namespace Bit.App.Controls
IsAntialias = true,
Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor.ToHex())
Color = SKColor.Parse(bgColor)
})
{
canvas.DrawCircle(midX, midY, radius, circlePaint);
@@ -108,7 +113,7 @@ namespace Bit.App.Controls
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
Color = SKColor.Parse(textColor.ToHex()),
Color = SKColor.Parse(textColor),
TextSize = textSize,
TextAlign = SKTextAlign.Center,
Typeface = typeface

View File

@@ -5,19 +5,19 @@ namespace Bit.App.Controls
{
public interface IAvatarImageSourcePool
{
AvatarImageSource GetOrCreateAvatar(string name, string email);
AvatarImageSource GetOrCreateAvatar(string userId, string name, string email, string color);
}
public class AvatarImageSourcePool : IAvatarImageSourcePool
{
private readonly ConcurrentDictionary<string, AvatarImageSource> _cache = new ConcurrentDictionary<string, AvatarImageSource>();
public AvatarImageSource GetOrCreateAvatar(string name, string email)
public AvatarImageSource GetOrCreateAvatar(string userId, string name, string email, string color)
{
var key = $"{name}{email}";
var key = $"{userId}{name}{email}{color}";
if (!_cache.TryGetValue(key, out var avatar))
{
avatar = new AvatarImageSource(name, email);
avatar = new AvatarImageSource(userId, name, email, color);
if (!_cache.TryAdd(key, avatar)
&&
!_cache.TryGetValue(key, out avatar)) // If add fails another thread created the avatar in between the first try get and the try add.

View File

@@ -21,13 +21,13 @@ namespace Bit.App.Controls
nameof(StrokeWidth), typeof(float), typeof(CircularProgressbarView), 3f);
public static readonly BindableProperty ProgressColorProperty = BindableProperty.Create(
nameof(ProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
nameof(ProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.FromHex("175DDC"));
public static readonly BindableProperty EndingProgressColorProperty = BindableProperty.Create(
nameof(EndingProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
nameof(EndingProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.FromHex("dd4b39"));
public static readonly BindableProperty BackgroundProgressColorProperty = BindableProperty.Create(
nameof(BackgroundProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
nameof(BackgroundProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.White);
public double Progress
{

View File

@@ -1,4 +1,6 @@
using Xamarin.Forms;
using System.Linq;
using Xamarin.CommunityToolkit.Converters;
using Xamarin.Forms;
namespace Bit.App.Controls
{
@@ -6,4 +8,13 @@ namespace Bit.App.Controls
{
public string ExtraDataForLogging { get; set; }
}
public class SelectionChangedEventArgsConverter : BaseNullableConverterOneWay<SelectionChangedEventArgs, object>
{
public override object? ConvertFrom(SelectionChangedEventArgs? value)
{
return value?.CurrentSelection.FirstOrDefault();
}
}
}

View File

@@ -5,7 +5,7 @@ namespace Bit.App.Controls
public class ExtendedSlider : Slider
{
public static readonly BindableProperty ThumbBorderColorProperty = BindableProperty.Create(
nameof(ThumbBorderColor), typeof(Color), typeof(ExtendedSlider), Color.Default);
nameof(ThumbBorderColor), typeof(Color), typeof(ExtendedSlider), Color.FromHex("b5b5b5"));
public Color ThumbBorderColor
{

View File

@@ -5,10 +5,10 @@ namespace Bit.App.Controls
public class ExtendedStepper : Stepper
{
public static readonly BindableProperty StepperBackgroundColorProperty = BindableProperty.Create(
nameof(StepperBackgroundColor), typeof(Color), typeof(ExtendedStepper), Color.Default);
nameof(StepperBackgroundColor), typeof(Color), typeof(ExtendedStepper), Color.White);
public static readonly BindableProperty StepperForegroundColorProperty = BindableProperty.Create(
nameof(StepperForegroundColor), typeof(Color), typeof(ExtendedStepper), Color.Default);
nameof(StepperForegroundColor), typeof(Color), typeof(ExtendedStepper), Color.Black);
public Color StepperBackgroundColor
{

View File

@@ -1,4 +1,5 @@
using Xamarin.Forms;
using Bit.App.Effects;
using Xamarin.Forms;
namespace Bit.App.Controls
{
@@ -16,6 +17,8 @@ namespace Bit.App.Controls
FontFamily = "bwi-font.ttf#bwi-font";
break;
}
Effects.Add(new RemoveFontPaddingEffect());
}
}
}

View File

@@ -1,4 +1,5 @@
using Xamarin.Forms;
using Bit.App.Effects;
using Xamarin.Forms;
namespace Bit.App.Controls
{
@@ -17,6 +18,8 @@ namespace Bit.App.Controls
FontFamily = "bwi-font.ttf#bwi-font";
break;
}
Effects.Add(new RemoveFontPaddingEffect());
}
}
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<Frame xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Controls.IconLabelButton"
xmlns:controls="clr-namespace:Bit.App.Controls"
x:Name="_iconLabelButton"
HeightRequest="45"
Padding="1"
StyleClass="btn-icon-secondary"
BackgroundColor="{Binding IconLabelBorderColor, Source={x:Reference _iconLabelButton}}"
BorderColor="Transparent"
HasShadow="False">
<Frame.GestureRecognizers>
<TapGestureRecognizer Command="{Binding ButtonCommand, Source={x:Reference _iconLabelButton}}" />
</Frame.GestureRecognizers>
<Frame
Margin="0"
Padding="0"
CornerRadius="{Binding CornerRadius, Source={x:Reference _iconLabelButton}}"
BackgroundColor="{Binding IconLabelBackgroundColor, Source={x:Reference _iconLabelButton}}"
BorderColor="Transparent"
IsClippedToBounds="True"
HasShadow="False">
<StackLayout
Orientation="Horizontal"
HorizontalOptions="Center">
<controls:IconLabel
VerticalOptions="Center"
HorizontalTextAlignment="Center"
FontSize="Large"
TextColor="{Binding IconLabelColor, Source={x:Reference _iconLabelButton}}"
Text="{Binding Icon, Source={x:Reference _iconLabelButton}}">
</controls:IconLabel>
<Label
VerticalOptions="Center"
HorizontalTextAlignment="Center"
TextColor="{Binding IconLabelColor, Source={x:Reference _iconLabelButton}}"
FontSize="Medium"
Text="{Binding Label, Source={x:Reference _iconLabelButton}}"/>
</StackLayout>
</Frame>
</Frame>

View File

@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.Core.Models.Domain;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Bit.App.Controls
{
public partial class IconLabelButton : Frame
{
public static readonly BindableProperty IconProperty = BindableProperty.Create(
nameof(Icon), typeof(string), typeof(IconLabelButton));
public static readonly BindableProperty LabelProperty = BindableProperty.Create(
nameof(Label), typeof(string), typeof(IconLabelButton));
public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
nameof(ButtonCommand), typeof(ICommand), typeof(IconLabelButton));
public static readonly BindableProperty IconLabelColorProperty = BindableProperty.Create(
nameof(IconLabelColor), typeof(Color), typeof(IconLabelButton), Color.White);
public static readonly BindableProperty IconLabelBackgroundColorProperty = BindableProperty.Create(
nameof(IconLabelBackgroundColor), typeof(Color), typeof(IconLabelButton), Color.White);
public static readonly BindableProperty IconLabelBorderColorProperty = BindableProperty.Create(
nameof(IconLabelBorderColor), typeof(Color), typeof(IconLabelButton), Color.White);
public IconLabelButton()
{
InitializeComponent();
}
public string Icon
{
get => GetValue(IconProperty) as string;
set => SetValue(IconProperty, value);
}
public string Label
{
get => GetValue(LabelProperty) as string;
set => SetValue(LabelProperty, value);
}
public ICommand ButtonCommand
{
get => GetValue(ButtonCommandProperty) as ICommand;
set => SetValue(ButtonCommandProperty, value);
}
public Color IconLabelColor
{
get { return (Color)GetValue(IconLabelColorProperty); }
set { SetValue(IconLabelColorProperty, value); }
}
public Color IconLabelBackgroundColor
{
get { return (Color)GetValue(IconLabelBackgroundColorProperty); }
set { SetValue(IconLabelBackgroundColorProperty, value); }
}
public Color IconLabelBorderColor
{
get { return (Color)GetValue(IconLabelBorderColorProperty); }
set { SetValue(IconLabelBorderColorProperty, value); }
}
}
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Bit.App.Controls
{
public interface IPasswordStrengthable
{
string Password { get; }
List<string> UserInputs { get; }
}
}

View File

@@ -0,0 +1,17 @@
using Bit.Core.Attributes;
namespace Bit.App.Controls
{
public enum PasswordStrengthLevel
{
[LocalizableEnum("Weak")]
VeryWeak,
[LocalizableEnum("Weak")]
Weak,
[LocalizableEnum("Good")]
Good,
[LocalizableEnum("Strong")]
Strong
}
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" ?>
<StackLayout
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="controls:PasswordStrengthViewModel"
x:Class="Bit.App.Controls.PasswordStrengthProgressBar"
StyleClass="box">
<StackLayout.Resources>
<ResourceDictionary>
<u:LocalizableEnumConverter x:Key="localizableEnum" />
</ResourceDictionary>
</StackLayout.Resources>
<ProgressBar
x:Name="_progressBar"
u:ProgressBarExtensions.AnimatedProgress="{Binding PasswordStrength}"
ScaleY="2" />
<Label
x:Name="_progressLabel"
Text="{Binding PasswordStrengthLevel, Converter={StaticResource localizableEnum}, TargetNullValue=' ' }"
StyleClass="box-footer-label" />
</StackLayout>

View File

@@ -0,0 +1,108 @@
using System;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public partial class PasswordStrengthProgressBar : StackLayout
{
public static readonly BindableProperty PasswordStrengthLevelProperty = BindableProperty.Create(
nameof(PasswordStrengthLevel),
typeof(PasswordStrengthLevel),
typeof(PasswordStrengthProgressBar),
propertyChanged: OnControlPropertyChanged);
public static readonly BindableProperty VeryWeakColorProperty = BindableProperty.Create(
nameof(VeryWeakColor),
typeof(Color),
typeof(PasswordStrengthProgressBar),
propertyChanged: OnControlPropertyChanged);
public static readonly BindableProperty WeakColorProperty = BindableProperty.Create(
nameof(WeakColor),
typeof(Color),
typeof(PasswordStrengthProgressBar),
propertyChanged: OnControlPropertyChanged);
public static readonly BindableProperty GoodColorProperty = BindableProperty.Create(
nameof(GoodColor),
typeof(Color),
typeof(PasswordStrengthProgressBar),
propertyChanged: OnControlPropertyChanged);
public static readonly BindableProperty StrongColorProperty = BindableProperty.Create(
nameof(StrongColor),
typeof(Color),
typeof(PasswordStrengthProgressBar),
propertyChanged: OnControlPropertyChanged);
public PasswordStrengthLevel? PasswordStrengthLevel
{
get { return (PasswordStrengthLevel?)GetValue(PasswordStrengthLevelProperty); }
set { SetValue(PasswordStrengthLevelProperty, value); }
}
public Color VeryWeakColor
{
get { return (Color)GetValue(VeryWeakColorProperty); }
set { SetValue(VeryWeakColorProperty, value); }
}
public Color WeakColor
{
get { return (Color)GetValue(WeakColorProperty); }
set { SetValue(WeakColorProperty, value); }
}
public Color GoodColor
{
get { return (Color)GetValue(GoodColorProperty); }
set { SetValue(GoodColorProperty, value); }
}
public Color StrongColor
{
get { return (Color)GetValue(StrongColorProperty); }
set { SetValue(StrongColorProperty, value); }
}
public PasswordStrengthProgressBar()
{
InitializeComponent();
SetBinding(PasswordStrengthProgressBar.PasswordStrengthLevelProperty, new Binding() { Path = nameof(PasswordStrengthViewModel.PasswordStrengthLevel) });
UpdateColors();
}
private static void OnControlPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
(bindable as PasswordStrengthProgressBar)?.UpdateColors();
}
public void UpdateColors()
{
if (_progressBar == null || _progressLabel == null)
{
return;
}
_progressBar.ProgressColor = GetColorForStrength();
_progressLabel.TextColor = _progressBar.ProgressColor;
}
private Color GetColorForStrength()
{
switch (PasswordStrengthLevel)
{
case Controls.PasswordStrengthLevel.VeryWeak:
return VeryWeakColor;
case Controls.PasswordStrengthLevel.Weak:
return WeakColor;
case Controls.PasswordStrengthLevel.Good:
return GoodColor;
case Controls.PasswordStrengthLevel.Strong:
return StrongColor;
default:
return Color.Transparent;
}
}
}
}

View File

@@ -0,0 +1,67 @@
using System.Collections.Generic;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class PasswordStrengthViewModel : ExtendedViewModel
{
private readonly IPasswordGenerationService _passwordGenerationService;
private readonly IPasswordStrengthable _passwordStrengthable;
private double _passwordStrength;
private Color _passwordColor;
private PasswordStrengthLevel? _passwordStrengthLevel;
public PasswordStrengthViewModel(IPasswordStrengthable passwordStrengthable)
{
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
_passwordStrengthable = passwordStrengthable;
}
public double PasswordStrength
{
get => _passwordStrength;
set => SetProperty(ref _passwordStrength, value);
}
public PasswordStrengthLevel? PasswordStrengthLevel
{
get => _passwordStrengthLevel;
set => SetProperty(ref _passwordStrengthLevel, value);
}
public List<string> GetPasswordStrengthUserInput(string email) => _passwordGenerationService.GetPasswordStrengthUserInput(email);
public void CalculatePasswordStrength()
{
if (string.IsNullOrEmpty(_passwordStrengthable.Password))
{
PasswordStrength = 0;
PasswordStrengthLevel = null;
return;
}
var passwordStrength = _passwordGenerationService.PasswordStrength(_passwordStrengthable.Password, _passwordStrengthable.UserInputs);
// The passwordStrength.Score is 0..4, convertion was made to be used as a progress directly by the control 0..1
PasswordStrength = (passwordStrength.Score + 1f) / 5f;
if (PasswordStrength <= 0.4f)
{
PasswordStrengthLevel = Controls.PasswordStrengthLevel.VeryWeak;
}
else if (PasswordStrength <= 0.6f)
{
PasswordStrengthLevel = Controls.PasswordStrengthLevel.Weak;
}
else if (PasswordStrength <= 0.8f)
{
PasswordStrengthLevel = Controls.PasswordStrengthLevel.Good;
}
else
{
PasswordStrengthLevel = Controls.PasswordStrengthLevel.Strong;
}
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
using Xamarin.Forms;
namespace Bit.App.Effects
{
public class RemoveFontPaddingEffect : RoutingEffect
{
public RemoveFontPaddingEffect()
: base("Bitwarden.RemoveFontPaddingEffect")
{ }
}
}

View File

@@ -0,0 +1,23 @@
using System;
namespace Bit.App.Models
{
public abstract class BaseNotificationData
{
public abstract string Type { get; }
public string Id { get; set; }
}
public class PasswordlessNotificationData : BaseNotificationData
{
public const string TYPE = "passwordlessNotificationData";
public override string Type => TYPE;
public int TimeoutInMinutes { get; set; }
public string UserEmail { get; set; }
}
}

View File

@@ -4,6 +4,8 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
@@ -147,8 +149,8 @@ namespace Bit.App.Pages
}
if (IsPolicyInEffect)
{
var userInput = await GetPasswordStrengthUserInput();
var passwordStrength = _passwordGenerationService.PasswordStrength(MasterPassword, userInput);
var userInputs = _passwordGenerationService.GetPasswordStrengthUserInput(await _stateService.GetEmailAsync());
var passwordStrength = _passwordGenerationService.PasswordStrength(MasterPassword, userInputs);
if (!await _policyService.EvaluateMasterPassword(passwordStrength.Score, MasterPassword, Policy))
{
await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordPolicyValidationMessage,
@@ -158,7 +160,7 @@ namespace Bit.App.Pages
}
else
{
if (MasterPassword.Length < 8)
if (MasterPassword.Length < Constants.MasterPasswordMinimumChars)
{
await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordLengthValMessage,
AppResources.MasterPasswordPolicyValidationTitle, AppResources.Ok);
@@ -174,19 +176,5 @@ namespace Bit.App.Pages
return true;
}
private async Task<List<string>> GetPasswordStrengthUserInput()
{
var email = await _stateService.GetEmailAsync();
List<string> userInput = null;
var atPosition = email.IndexOf('@');
if (atPosition > -1)
{
var rx = new Regex("/[^A-Za-z0-9]/", RegexOptions.Compiled);
var data = rx.Split(email.Substring(0, atPosition).Trim().ToLower());
userInput = new List<string>(data);
}
return userInput;
}
}
}

View File

@@ -6,11 +6,12 @@ namespace Bit.App.Pages
{
private HintPageViewModel _vm;
public HintPage()
public HintPage(string email = null)
{
InitializeComponent();
_vm = BindingContext as HintPageViewModel;
_vm.Page = this;
_vm.Email = email;
if (Device.RuntimePlatform == Device.Android)
{
ToolbarItems.RemoveAt(0);

View File

@@ -15,6 +15,7 @@ namespace Bit.App.Pages
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IApiService _apiService;
private readonly ILogger _logger;
private string _email;
public HintPageViewModel()
{
@@ -34,7 +35,12 @@ namespace Bit.App.Pages
}
public ICommand SubmitCommand { get; }
public string Email { get; set; }
public string Email
{
get => _email;
set => SetProperty(ref _email, value);
}
public async Task SubmitAsync()
{

View File

@@ -24,6 +24,7 @@
UseOriginalImage="True"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Account}" />
<ToolbarItem x:Name="_closeButton" Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"/>
<ToolbarItem
Icon="cog_environment.png" Clicked="Environment_Clicked" Order="Primary"
AutomationProperties.IsInAccessibleTree="True"
@@ -32,30 +33,68 @@
<ContentPage.Resources>
<ResourceDictionary>
<StackLayout x:Name="_mainLayout" x:Key="mainLayout" Spacing="0" Padding="10, 5">
<StackLayout VerticalOptions="CenterAndExpand" Spacing="20">
<Image
x:Name="_logo"
Source="logo.png"
VerticalOptions="Center" />
<Label Text="{u:I18n LoginOrCreateNewAccount}"
StyleClass="text-lg"
HorizontalTextAlignment="Center">
</Label>
<StackLayout Spacing="5">
<Button Text="{u:I18n LogIn}"
StyleClass="btn-primary"
Clicked="LogIn_Clicked" />
<Button Text="{u:I18n CreateAccount}"
Clicked="Register_Clicked" />
<Button Text="{u:I18n LogInSso}"
Clicked="LogInSso_Clicked" />
<Button Text="{u:I18n Cancel}"
IsVisible="{Binding ShowCancelButton}"
Margin="0,10,0,0"
Clicked="Cancel_Clicked" />
<u:InverseBoolConverter x:Key="inverseBool" />
<StackLayout x:Name="_mainLayout" x:Key="mainLayout" Spacing="30" Padding="20, 50, 20, 0">
<Image
x:Name="_logo"
Source="logo.png"
VerticalOptions="Center" />
<Label Text="{u:I18n LoginOrCreateNewAccount}"
StyleClass="text-lg"
HorizontalTextAlignment="Center"/>
<StackLayout
StyleClass="box-row">
<Label
Text="{u:I18n EmailAddress}"
StyleClass="box-label" />
<Entry
x:Name="_email"
Text="{Binding Email}"
Keyboard="Email"
StyleClass="box-value"
ReturnType="Go"
ReturnCommand="{Binding ContinueCommand}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{DynamicResource MutedColor}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Entry>
<StackLayout
Orientation="Horizontal"
Margin="0, 16, 0 ,0">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding RememberEmailCommand}" />
</StackLayout.GestureRecognizers>
<Label
Text="{u:I18n RememberMe}"
StyleClass="text-sm"
HorizontalOptions="FillAndExpand"
VerticalOptions="Center"
VerticalTextAlignment="Center"/>
<Switch
Scale="0.8"
IsToggled="{Binding RememberEmail}"
VerticalOptions="Center"/>
</StackLayout>
</StackLayout>
<Button Text="{u:I18n Continue}"
StyleClass="btn-primary"
IsEnabled="{Binding CanContinue}"
Command="{Binding ContinueCommand}" />
<Label FormattedText="{Binding CreateAccountText}"
Margin="0, 10"
StyleClass="box-footer-label">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding CreateAccountCommand}" />
</Label.GestureRecognizers>
</Label>
</StackLayout>
</ResourceDictionary>
</ContentPage.Resources>

View File

@@ -10,6 +10,7 @@ namespace Bit.App.Pages
{
public partial class HomePage : BaseContentPage
{
private bool _checkRememberedEmail;
private readonly HomeViewModel _vm;
private readonly AppOptions _appOptions;
private IBroadcasterService _broadcasterService;
@@ -21,17 +22,22 @@ namespace Bit.App.Pages
InitializeComponent();
_vm = BindingContext as HomeViewModel;
_vm.Page = this;
_vm.StartLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartLoginAsync());
_vm.ShowCancelButton = _appOptions?.IosExtension ?? false;
_vm.StartLoginAction = async () => await StartLoginAsync();
_vm.StartRegisterAction = () => Device.BeginInvokeOnMainThread(async () => await StartRegisterAsync());
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
_vm.StartEnvironmentAction = () => Device.BeginInvokeOnMainThread(async () => await StartEnvironmentAsync());
_vm.CloseAction = async () =>
{
await _accountListOverlay.HideAsync();
await Navigation.PopModalAsync();
};
UpdateLogo();
if (_appOptions?.IosExtension ?? false)
if (!_vm.ShowCancelButton)
{
_vm.ShowCancelButton = true;
ToolbarItems.Remove(_closeButton);
}
if (_appOptions?.HideAccountSwitcher ?? false)
{
ToolbarItems.Remove(_accountAvatar);
@@ -52,7 +58,7 @@ namespace Bit.App.Pages
if (!_appOptions?.HideAccountSwitcher ?? false)
{
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
_vm.AvatarImageSource = await GetAvatarImageSourceAsync(false);
}
_broadcasterService.Subscribe(nameof(HomePage), (message) =>
{
@@ -96,28 +102,12 @@ namespace Bit.App.Pages
}
}
private void LogIn_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.StartLoginAction();
}
}
private async Task StartLoginAsync()
{
var page = new LoginPage(null, _appOptions);
var page = new LoginPage(_vm.Email, _appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private void Register_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.StartRegisterAction();
}
}
private async Task StartRegisterAsync()
{
var page = new RegisterPage(this);

View File

@@ -1,8 +1,15 @@
using System;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
@@ -12,19 +19,37 @@ namespace Bit.App.Pages
private readonly IMessagingService _messagingService;
private bool _showCancelButton;
private bool _rememberEmail;
private string _email;
private bool _isEmailEnabled;
private bool _canLogin;
private IPlatformUtilsService _platformUtilsService;
private ILogger _logger;
private IEnvironmentService _environmentService;
private IAccountsManager _accountManager;
public HomeViewModel()
{
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
var logger = ServiceContainer.Resolve<ILogger>("logger");
_stateService = ServiceContainer.Resolve<IStateService>();
_messagingService = ServiceContainer.Resolve<IMessagingService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
_logger = ServiceContainer.Resolve<ILogger>();
_environmentService = ServiceContainer.Resolve<IEnvironmentService>();
_accountManager = ServiceContainer.Resolve<IAccountsManager>();
PageTitle = AppResources.Bitwarden;
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, logger)
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
{
AllowActiveAccountSelection = true
};
RememberEmailCommand = new Command(() => RememberEmail = !RememberEmail);
ContinueCommand = new AsyncCommand(ContinueToLoginStepAsync, allowsMultipleExecutions: false);
CreateAccountCommand = new AsyncCommand(async () => await Device.InvokeOnMainThreadAsync(StartRegisterAction),
onException: _logger.Exception, allowsMultipleExecutions: false);
CloseCommand = new AsyncCommand(async () => await Device.InvokeOnMainThreadAsync(CloseAction),
onException: _logger.Exception, allowsMultipleExecutions: false);
InitAsync().FireAndForget();
}
public bool ShowCancelButton
@@ -33,11 +58,91 @@ namespace Bit.App.Pages
set => SetProperty(ref _showCancelButton, value);
}
public bool RememberEmail
{
get => _rememberEmail;
set => SetProperty(ref _rememberEmail, value);
}
public string Email
{
get => _email;
set => SetProperty(ref _email, value,
additionalPropertyNames: new[] { nameof(CanContinue) });
}
public bool CanContinue => !string.IsNullOrEmpty(Email);
public FormattedString CreateAccountText
{
get
{
var fs = new FormattedString();
fs.Spans.Add(new Span
{
Text = $"{AppResources.NewAroundHere} "
});
fs.Spans.Add(new Span
{
Text = AppResources.CreateAccount,
TextColor = ThemeManager.GetResourceColor("PrimaryColor")
});
return fs;
}
}
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
public Action StartLoginAction { get; set; }
public Action StartRegisterAction { get; set; }
public Action StartSsoLoginAction { get; set; }
public Action StartEnvironmentAction { get; set; }
public Action CloseAction { get; set; }
public Command RememberEmailCommand { get; set; }
public AsyncCommand ContinueCommand { get; }
public AsyncCommand CloseCommand { get; }
public AsyncCommand CreateAccountCommand { get; }
public async Task InitAsync()
{
Email = await _stateService.GetRememberedEmailAsync();
RememberEmail = !string.IsNullOrEmpty(Email);
}
public async Task ContinueToLoginStepAsync()
{
try
{
if (string.IsNullOrWhiteSpace(Email))
{
await _platformUtilsService.ShowDialogAsync(
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
AppResources.AnErrorHasOccurred, AppResources.Ok);
return;
}
if (!Email.Contains("@"))
{
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidEmail, AppResources.AnErrorHasOccurred,
AppResources.Ok);
return;
}
await _stateService.SetRememberedEmailAsync(RememberEmail ? Email : null);
var userId = await _stateService.GetUserIdAsync(Email);
if (!string.IsNullOrWhiteSpace(userId) &&
(await _stateService.GetEnvironmentUrlsAsync(userId))?.Base == _environmentService.BaseUrl &&
await _stateService.IsAuthenticatedAsync(userId))
{
await _accountManager.PromptToSwitchToExistingAccountAsync(userId);
return;
}
StartLoginAction();
}
catch (Exception ex)
{
_logger.Exception(ex);
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred, AppResources.Ok);
}
}
}
}

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);
@@ -226,8 +228,7 @@ namespace Bit.App.Pages
}
ShowPassword = false;
var kdf = await _stateService.GetKdfTypeAsync();
var kdfIterations = await _stateService.GetKdfIterationsAsync();
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
if (PinLock)
{
@@ -237,7 +238,7 @@ namespace Bit.App.Pages
if (_isPinProtected)
{
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email,
kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000),
kdfConfig,
await _stateService.GetPinProtectedKeyAsync());
var encKey = await _cryptoService.GetEncKeyAsync(key);
var protectedPin = await _stateService.GetProtectedPinAsync();
@@ -252,8 +253,7 @@ namespace Bit.App.Pages
}
else
{
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email,
kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000));
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email, kdfConfig);
failed = false;
Pin = string.Empty;
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
@@ -278,7 +278,7 @@ namespace Bit.App.Pages
}
else
{
var key = await _cryptoService.MakeKeyAsync(MasterPassword, _email, kdf, kdfIterations);
var key = await _cryptoService.MakeKeyAsync(MasterPassword, _email, kdfConfig);
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
var passwordValid = false;
@@ -312,8 +312,7 @@ namespace Bit.App.Pages
var protectedPin = await _stateService.GetProtectedPinAsync();
var encKey = await _cryptoService.GetEncKeyAsync(key);
var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey);
var pinKey = await _cryptoService.MakePinKeyAysnc(decPin, _email,
kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000));
var pinKey = await _cryptoService.MakePinKeyAysnc(decPin, _email, kdfConfig);
await _stateService.SetPinProtectedKeyAsync(await _cryptoService.EncryptAsync(key.Key, pinKey));
}
MasterPassword = string.Empty;
@@ -387,6 +386,7 @@ namespace Bit.App.Pages
private async Task DoContinueAsync()
{
await _stateService.SetBiometricLockedAsync(false);
_watchDeviceService.SyncDataToWatchAsync().FireAndForget();
_messagingService.Send("unlocked");
UnlockedAction?.Invoke();
}

View File

@@ -4,6 +4,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.LoginPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:LoginPageViewModel"
@@ -46,33 +47,13 @@
Order="Secondary" />
<ScrollView x:Name="_mainLayout" x:Key="mainLayout">
<StackLayout Spacing="20">
<StackLayout Spacing="0">
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row">
<Label
Text="{u:I18n EmailAddress}"
StyleClass="box-label" />
<Entry
x:Name="_email"
Text="{Binding Email}"
IsEnabled="{Binding IsEmailEnabled}"
Keyboard="Email"
StyleClass="box-value">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{DynamicResource MutedColor}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Entry>
</StackLayout>
<Grid StyleClass="box-row">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
@@ -81,6 +62,7 @@
<Label
Text="{u:I18n MasterPassword}"
StyleClass="box-label"
Padding="0, 10, 0, 0"
Grid.Row="0"
Grid.Column="0" />
<controls:MonoEntry
@@ -98,21 +80,60 @@
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
Command="{Binding TogglePasswordCommand}"
Grid.Row="0"
Grid.Row="1"
Grid.Column="1"
Grid.RowSpan="2"
Grid.RowSpan="1"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
<Label
Text="{u:I18n GetMasterPasswordwordHint}"
StyleClass="box-footer-label"
TextColor="{DynamicResource HyperlinkColor}"
Padding="0,5,0,0"
Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="Hint_Clicked" />
</Label.GestureRecognizers>
</Label>
</Grid>
</StackLayout>
<StackLayout Padding="10, 0">
<Button Text="{u:I18n LogIn}"
<StackLayout Padding="10, 10">
<Button x:Name="_loginWithMasterPassword"
Text="{u:I18n LogInWithMasterPassword}"
StyleClass="btn-primary"
Clicked="LogIn_Clicked" />
<Button Text="{u:I18n Cancel}"
IsVisible="{Binding ShowCancelButton}"
Clicked="Cancel_Clicked" />
<controls:IconLabelButton
HorizontalOptions="Fill"
VerticalOptions="CenterAndExpand"
Icon="{Binding Source={x:Static core:BitwardenIcons.Device}}"
Label="{u:I18n LogInWithAnotherDevice}"
ButtonCommand="{Binding LogInWithDeviceCommand}"
IsVisible="{Binding IsKnownDevice}"/>
<controls:IconLabelButton
HorizontalOptions="Fill"
VerticalOptions="CenterAndExpand"
Icon="{Binding Source={x:Static core:BitwardenIcons.Suitcase}}"
Label="{u:I18n LogInSso}">
<controls:IconLabelButton.GestureRecognizers>
<TapGestureRecognizer Tapped="LogInSSO_Clicked" />
</controls:IconLabelButton.GestureRecognizers>
</controls:IconLabelButton>
<Label
Text="{Binding LoggingInAsText}"
StyleClass="text-sm"
Margin="0,40,0,0"/>
<Label
Text="{u:I18n NotYou}"
StyleClass="text-md"
HorizontalOptions="Start"
TextColor="{DynamicResource HyperlinkColor}">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="Cancel_Clicked" />
</Label.GestureRecognizers>
</Label>
</StackLayout>
</StackLayout>
</ScrollView>

View File

@@ -3,7 +3,9 @@ using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -25,8 +27,9 @@ namespace Bit.App.Pages
_vm.Page = this;
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
_vm.UpdateTempPasswordAction =
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
_vm.LogInWithDeviceAction = () => StartLoginWithDeviceAsync().FireAndForget();
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
_vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
_vm.CloseAction = async () =>
{
await _accountListOverlay.HideAsync();
@@ -42,9 +45,6 @@ namespace Bit.App.Pages
_vm.Email = email;
MasterPasswordEntry = _masterPassword;
_email.ReturnType = ReturnType.Next;
_email.ReturnCommand = new Command(() => _masterPassword.Focus());
if (Device.RuntimePlatform == Device.iOS)
{
ToolbarItems.Add(_moreItem);
@@ -54,11 +54,6 @@ namespace Bit.App.Pages
ToolbarItems.Add(_getPasswordHint);
}
if (Device.RuntimePlatform == Device.Android && !_vm.IsEmailEnabled)
{
ToolbarItems.Add(_removeAccount);
}
if (_appOptions?.IosExtension ?? false)
{
_vm.ShowCancelButton = true;
@@ -78,16 +73,20 @@ namespace Bit.App.Pages
_mainContent.Content = _mainLayout;
_accountAvatar?.OnAppearing();
await _vm.InitAsync();
if (!_appOptions?.HideAccountSwitcher ?? false)
{
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
_vm.AvatarImageSource = await GetAvatarImageSourceAsync(_vm.EmailIsInSavedAccounts);
}
await _vm.InitAsync();
if (!_inputFocused)
{
RequestFocus(string.IsNullOrWhiteSpace(_vm.Email) ? _email : _masterPassword);
RequestFocus(_masterPassword);
_inputFocused = true;
}
if (Device.RuntimePlatform == Device.Android && !_vm.CanRemoveAccount)
{
ToolbarItems.Add(_removeAccount);
}
}
protected override bool OnBackButtonPressed()
@@ -115,11 +114,31 @@ namespace Bit.App.Pages
}
}
private void LogInSSO_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.StartSsoLoginAction();
}
}
private async Task StartLoginWithDeviceAsync()
{
var page = new LoginPasswordlessRequestPage(_vm.Email, _appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task StartSsoLoginAsync()
{
var page = new LoginSsoPage(_appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private void Hint_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
Navigation.PushModalAsync(new NavigationPage(new HintPage()));
_vm.ShowMasterPasswordHintAsync().FireAndForget();
}
}

View File

@@ -1,13 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.App.Utilities.AccountManagement;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Models.View;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
@@ -25,12 +31,15 @@ namespace Bit.App.Pages
private readonly II18nService _i18nService;
private readonly IMessagingService _messagingService;
private readonly ILogger _logger;
private readonly IApiService _apiService;
private readonly IAppIdService _appIdService;
private readonly IAccountsManager _accountManager;
private bool _showPassword;
private bool _showCancelButton;
private string _email;
private string _masterPassword;
private bool _isEmailEnabled;
private bool _isKnownDevice;
public LoginPageViewModel()
{
@@ -43,11 +52,15 @@ namespace Bit.App.Pages
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
_apiService = ServiceContainer.Resolve<IApiService>();
_appIdService = ServiceContainer.Resolve<IAppIdService>();
_accountManager = ServiceContainer.Resolve<IAccountsManager>();
PageTitle = AppResources.Bitwarden;
TogglePasswordCommand = new Command(TogglePassword);
LogInCommand = new Command(async () => await LogInAsync());
MoreCommand = new AsyncCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
LogInWithDeviceCommand = new AsyncCommand(() => Device.InvokeOnMainThreadAsync(LogInWithDeviceAction), onException: _logger.Exception, allowsMultipleExecutions: false);
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
{
@@ -76,7 +89,11 @@ namespace Bit.App.Pages
public string Email
{
get => _email;
set => SetProperty(ref _email, value);
set => SetProperty(ref _email, value,
additionalPropertyNames: new string[]
{
nameof(LoggingInAsText)
});
}
public string MasterPassword
@@ -91,20 +108,31 @@ namespace Bit.App.Pages
set => SetProperty(ref _isEmailEnabled, value);
}
public bool IsIosExtension { get; set; }
public bool IsKnownDevice
{
get => _isKnownDevice;
set => SetProperty(ref _isKnownDevice, value);
}
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
public Command LogInCommand { get; }
public Command TogglePasswordCommand { get; }
public ICommand MoreCommand { get; internal set; }
public ICommand LogInWithDeviceCommand { get; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string LoggingInAsText => string.Format(AppResources.LoggingInAsX, Email);
public bool IsIosExtension { get; set; }
public bool CanRemoveAccount { get; set; }
public Action StartTwoFactorAction { get; set; }
public Action LogInSuccessAction { get; set; }
public Action LogInWithDeviceAction { get; set; }
public Action UpdateTempPasswordAction { get; set; }
public Action StartSsoLoginAction { get; set; }
public Action CloseAction { get; set; }
public bool EmailIsInSavedAccounts => _stateService.AccountViews != null && _stateService.AccountViews.Any(e => e.Email == Email);
protected override II18nService i18nService => _i18nService;
protected override IEnvironmentService environmentService => _environmentService;
protected override IDeviceActionService deviceActionService => _deviceActionService;
@@ -112,9 +140,22 @@ namespace Bit.App.Pages
public async Task InitAsync()
{
if (string.IsNullOrWhiteSpace(Email))
try
{
Email = await _stateService.GetRememberedEmailAsync();
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
await AccountSwitchingOverlayViewModel.RefreshAccountViewsAsync();
if (string.IsNullOrWhiteSpace(Email))
{
Email = await _stateService.GetRememberedEmailAsync();
}
var deviceIdentifier = await _appIdService.GetAppIdAsync();
IsKnownDevice = await _apiService.GetKnownDeviceAsync(Email, deviceIdentifier);
CanRemoveAccount = await _stateService.GetActiveUserEmailAsync() != Email;
await _deviceActionService.HideLoadingAsync();
}
catch (Exception ex)
{
HandleException(ex);
}
}
@@ -158,7 +199,7 @@ namespace Bit.App.Pages
var userEnvUrls = await _stateService.GetEnvironmentUrlsAsync(userId);
if (userEnvUrls?.Base == _environmentService.BaseUrl)
{
await PromptToSwitchToExistingAccountAsync(userId);
await _accountManager.PromptToSwitchToExistingAccountAsync(userId);
return;
}
}
@@ -170,7 +211,6 @@ namespace Bit.App.Pages
}
var response = await _authService.LogInAsync(Email, MasterPassword, _captchaToken);
await _stateService.SetRememberedEmailAsync(Email);
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
if (response.CaptchaNeeded)
@@ -216,19 +256,14 @@ namespace Bit.App.Pages
private async Task MoreAsync()
{
var buttons = IsEmailEnabled
var buttons = IsEmailEnabled || CanRemoveAccount
? new[] { AppResources.GetPasswordHint }
: new[] { AppResources.GetPasswordHint, AppResources.RemoveAccount };
var selection = await _deviceActionService.DisplayActionSheetAsync(AppResources.Options, AppResources.Cancel, null, buttons);
if (selection == AppResources.GetPasswordHint)
{
var hintNavigationPage = new NavigationPage(new HintPage());
if (IsIosExtension)
{
ThemeManager.ApplyResourcesTo(hintNavigationPage);
}
await Page.Navigation.PushModalAsync(hintNavigationPage);
await ShowMasterPasswordHintAsync();
}
else if (selection == AppResources.RemoveAccount)
{
@@ -236,6 +271,16 @@ namespace Bit.App.Pages
}
}
public async Task ShowMasterPasswordHintAsync()
{
var hintNavigationPage = new NavigationPage(new HintPage(Email));
if (IsIosExtension)
{
ThemeManager.ApplyResourcesTo(hintNavigationPage);
}
await Page.Navigation.PushModalAsync(hintNavigationPage);
}
public void TogglePassword()
{
ShowPassword = !ShowPassword;
@@ -260,17 +305,5 @@ namespace Bit.App.Pages
_logger.Exception(e);
}
}
private async Task PromptToSwitchToExistingAccountAsync(string userId)
{
var switchToAccount = await _platformUtilsService.ShowDialogAsync(
AppResources.SwitchToAlreadyAddedAccountConfirmation,
AppResources.AccountAlreadyAdded, AppResources.Yes, AppResources.Cancel);
if (switchToAccount)
{
await _stateService.SetActiveUserAsync(userId);
_messagingService.Send("switchedAccount");
}
}
}
}

View File

@@ -14,10 +14,7 @@ namespace Bit.App.Pages
_vm.LoginRequest = loginPasswordlessDetails;
if (Device.RuntimePlatform == Device.iOS)
{
ToolbarItems.Add(_closeItem);
}
ToolbarItems.Add(_closeItem);
}
private async void Close_Clicked(object sender, System.EventArgs e)

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" ?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.LoginPasswordlessRequestPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:LoginPasswordlessRequestViewModel"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:LoginPasswordlessRequestViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1" x:Name="_closeItem"/>
</ContentPage.ToolbarItems>
<ScrollView>
<StackLayout
Padding="7, 0, 7, 20">
<Label
Text="{u:I18n LogInInitiated}"
FontSize="Title"
FontAttributes="Bold"
Margin="0,14,0,21"/>
<Label
Text="{u:I18n ANotificationHasBeenSentToYourDevice}"
FontSize="Small"
Margin="0,0,0,10"/>
<Label
Text="{u:I18n PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice}"
FontSize="Small"
Margin="0,0,0,24"/>
<Label
Text="{u:I18n FingerprintPhrase}"
FontSize="Small"
FontAttributes="Bold"/>
<controls:MonoLabel
FormattedText="{Binding FingerprintPhrase}"
FontSize="Medium"
TextColor="{DynamicResource FingerprintPhrase}"/>
<Label
Text="{u:I18n ResendNotification}"
StyleClass="text-md"
HorizontalOptions="Start"
Margin="0,40,0,0"
TextColor="{DynamicResource HyperlinkColor}">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding CreatePasswordlessLoginCommand}" />
</Label.GestureRecognizers>
</Label>
<StackLayout
Orientation="Horizontal"
Margin="0,30,0,0">
<Label
Text="{u:I18n NeedAnotherOption}"
FontSize="Small"
VerticalTextAlignment="End"/>
<Label
Text="{u:I18n ViewAllLoginOptions}"
StyleClass="text-md"
VerticalTextAlignment="End"
VerticalOptions="CenterAndExpand"
Margin="5, 0"
TextColor="{DynamicResource HyperlinkColor}">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding CloseCommand}" />
</Label.GestureRecognizers>
</Label>
</StackLayout>
</StackLayout>
</ScrollView>
</pages:BaseContentPage>

View File

@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class LoginPasswordlessRequestPage : BaseContentPage
{
private LoginPasswordlessRequestViewModel _vm;
private readonly AppOptions _appOptions;
public LoginPasswordlessRequestPage(string email, AppOptions appOptions = null)
{
InitializeComponent();
_appOptions = appOptions;
_vm = BindingContext as LoginPasswordlessRequestViewModel;
_vm.Page = this;
_vm.Email = email;
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
_vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
_vm.CloseAction = () => { Navigation.PopModalAsync(); };
_vm.CreatePasswordlessLoginCommand.Execute(null);
}
protected override void OnAppearing()
{
base.OnAppearing();
_vm.StartCheckLoginRequestStatus();
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_vm.StopCheckLoginRequestStatus();
}
private async Task StartTwoFactorAsync()
{
var page = new TwoFactorPage(false, _appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task LogInSuccessAsync()
{
if (AppHelpers.SetAlternateMainPage(_appOptions))
{
return;
}
var previousPage = await AppHelpers.ClearPreviousPage();
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
}
private async Task UpdateTempPasswordAsync()
{
var page = new UpdateTempPasswordPage();
await Navigation.PushModalAsync(new NavigationPage(page));
}
}
}

View File

@@ -0,0 +1,194 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class LoginPasswordlessRequestViewModel : CaptchaProtectedViewModel
{
private const int REQUEST_TIME_UPDATE_PERIOD_IN_SECONDS = 4;
private IDeviceActionService _deviceActionService;
private IAuthService _authService;
private ISyncService _syncService;
private II18nService _i18nService;
private IStateService _stateService;
private IPlatformUtilsService _platformUtilsService;
private IEnvironmentService _environmentService;
private ILogger _logger;
protected override II18nService i18nService => _i18nService;
protected override IEnvironmentService environmentService => _environmentService;
protected override IDeviceActionService deviceActionService => _deviceActionService;
protected override IPlatformUtilsService platformUtilsService => _platformUtilsService;
private CancellationTokenSource _checkLoginRequestStatusCts;
private Task _checkLoginRequestStatusTask;
private string _fingerprintPhrase;
private string _email;
private string _requestId;
private string _requestAccessCode;
// Item1 publicKey, Item2 privateKey
private Tuple<byte[], byte[]> _requestKeyPair;
public LoginPasswordlessRequestViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
_environmentService = ServiceContainer.Resolve<IEnvironmentService>();
_authService = ServiceContainer.Resolve<IAuthService>();
_syncService = ServiceContainer.Resolve<ISyncService>();
_i18nService = ServiceContainer.Resolve<II18nService>();
_stateService = ServiceContainer.Resolve<IStateService>();
_logger = ServiceContainer.Resolve<ILogger>();
PageTitle = AppResources.LogInWithAnotherDevice;
CreatePasswordlessLoginCommand = new AsyncCommand(CreatePasswordlessLoginAsync,
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
CloseCommand = new AsyncCommand(() => Device.InvokeOnMainThreadAsync(CloseAction),
onException: _logger.Exception,
allowsMultipleExecutions: false);
}
public Action StartTwoFactorAction { get; set; }
public Action LogInSuccessAction { get; set; }
public Action UpdateTempPasswordAction { get; set; }
public Action CloseAction { get; set; }
public ICommand CreatePasswordlessLoginCommand { get; }
public ICommand CloseCommand { get; }
public string FingerprintPhrase
{
get => _fingerprintPhrase;
set => SetProperty(ref _fingerprintPhrase, value);
}
public string Email
{
get => _email;
set => SetProperty(ref _email, value);
}
public void StartCheckLoginRequestStatus()
{
try
{
_checkLoginRequestStatusCts?.Cancel();
_checkLoginRequestStatusCts = new CancellationTokenSource();
_checkLoginRequestStatusTask = new TimerTask(_logger, CheckLoginRequestStatus, _checkLoginRequestStatusCts).RunPeriodic(TimeSpan.FromSeconds(REQUEST_TIME_UPDATE_PERIOD_IN_SECONDS));
}
catch (Exception ex)
{
_logger.Exception(ex);
}
}
public void StopCheckLoginRequestStatus()
{
try
{
_checkLoginRequestStatusCts?.Cancel();
_checkLoginRequestStatusCts?.Dispose();
_checkLoginRequestStatusCts = null;
}
catch (Exception ex)
{
_logger.Exception(ex);
}
}
private async Task CheckLoginRequestStatus()
{
if (string.IsNullOrEmpty(_requestId) || string.IsNullOrEmpty(_requestAccessCode))
{
return;
}
try
{
var response = await _authService.GetPasswordlessLoginResponseAsync(_requestId, _requestAccessCode);
if (response.RequestApproved == null || !response.RequestApproved.Value)
{
return;
}
StopCheckLoginRequestStatus();
var authResult = await _authService.LogInPasswordlessAsync(Email, _requestAccessCode, _requestId, _requestKeyPair.Item2, response.Key, response.MasterPasswordHash);
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
if (await HandleCaptchaAsync(authResult.CaptchaSiteKey, authResult.CaptchaNeeded, CheckLoginRequestStatus))
{
return;
}
if (authResult.TwoFactor)
{
StartTwoFactorAction?.Invoke();
}
else if (authResult.ForcePasswordReset)
{
UpdateTempPasswordAction?.Invoke();
}
else
{
_syncService.FullSyncAsync(true).FireAndForget();
LogInSuccessAction?.Invoke();
}
}
catch (Exception ex)
{
StartCheckLoginRequestStatus();
HandleException(ex);
}
}
private async Task CreatePasswordlessLoginAsync()
{
await Device.InvokeOnMainThreadAsync(() => _deviceActionService.ShowLoadingAsync(AppResources.Loading));
var response = await _authService.PasswordlessCreateLoginRequestAsync(_email);
if (response != null)
{
FingerprintPhrase = response.RequestFingerprint;
_requestId = response.Id;
_requestAccessCode = response.RequestAccessCode;
_requestKeyPair = response.RequestKeyPair;
}
await _deviceActionService.HideLoadingAsync();
}
private void HandleException(Exception ex)
{
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>
{
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage);
}).FireAndForget();
_logger.Exception(ex);
}
}
}

View File

@@ -100,7 +100,7 @@ namespace Bit.App.Pages
private async Task UpdateRequestTime()
{
TriggerPropertyChanged(nameof(TimeOfRequestText));
if (DateTime.UtcNow > LoginRequest?.RequestDate.AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes))
if (LoginRequest?.IsExpired ?? false)
{
StopRequestTimeUpdater();
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
@@ -110,13 +110,21 @@ namespace Bit.App.Pages
private async Task PasswordlessLoginAsync(bool approveRequest)
{
if (LoginRequest.RequestDate.AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) <= DateTime.UtcNow)
if (LoginRequest.IsExpired)
{
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
await Page.Navigation.PopModalAsync();
return;
}
var loginRequestData = await _authService.GetPasswordlessLoginRequestByIdAsync(LoginRequest.Id);
if (loginRequestData.IsAnswered)
{
await _platformUtilsService.ShowDialogAsync(AppResources.ThisRequestIsNoLongerValid);
await Page.Navigation.PopModalAsync();
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
await _authService.PasswordlessLoginAsync(LoginRequest.Id, LoginRequest.PubKey, approveRequest);
await _deviceActionService.HideLoadingAsync();
@@ -140,16 +148,6 @@ namespace Bit.App.Pages
return string.Format(AppResources.XMinutesAgo, DateTime.UtcNow.Minute - requestDate.Value.ToUniversalTime().Minute);
}
private void HandleException(Exception ex)
{
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>
{
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage);
}).FireAndForget();
_logger.Exception(ex);
}
}
public class LoginPasswordlessDetails
@@ -171,5 +169,7 @@ namespace Bit.App.Pages
public string DeviceType { get; set; }
public string IpAddress { get; set; }
public bool IsExpired => RequestDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) < DateTime.UtcNow;
}
}

View File

@@ -25,7 +25,7 @@
</ContentPage.ToolbarItems>
<ScrollView>
<StackLayout Spacing="20">
<StackLayout Spacing="10">
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row">
<Label
@@ -72,8 +72,19 @@
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
</Grid>
<Label
Text="{u:I18n MasterPasswordDescription}"
StyleClass="box-footer-label" />
StyleClass="box-sub-label"
Margin="0,0,0,10">
<Label.FormattedText>
<FormattedString>
<Span Text="{u:I18n Important}" TextColor="{DynamicResource InfoColor}"/>
<Span Text=": " TextColor="{DynamicResource InfoColor}"/>
<Span Text="{Binding MasterPasswordMininumCharactersDescription}" TextColor="{DynamicResource MutedColor}"/>
</FormattedString>
</Label.FormattedText>
</Label>
<controls:PasswordStrengthProgressBar
BindingContext="{Binding PasswordStrengthViewModel}"
Margin="0,0"/>
</StackLayout>
<StackLayout StyleClass="box">
<Grid StyleClass="box-row">
@@ -126,6 +137,17 @@
StyleClass="box-footer-label" />
</StackLayout>
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-switch">
<Switch
IsToggled="{Binding CheckExposedMasterPassword}"
StyleClass="box-value"
HorizontalOptions="Start"
Margin="0, 0, 10, 0"/>
<Label
Text="{u:I18n CheckKnownDataBreachesForThisPassword}"
StyleClass="box-footer-label"
VerticalOptions="Center"/>
</StackLayout>
<StackLayout StyleClass="box-row, box-row-switch"
IsVisible="{Binding ShowTerms}">
<Switch

View File

@@ -1,28 +1,36 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Request;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class RegisterPageViewModel : CaptchaProtectedViewModel
public class RegisterPageViewModel : CaptchaProtectedViewModel, IPasswordStrengthable
{
private readonly IDeviceActionService _deviceActionService;
private readonly II18nService _i18nService;
private readonly IEnvironmentService _environmentService;
private readonly IAuditService _auditService;
private readonly IApiService _apiService;
private readonly ICryptoService _cryptoService;
private readonly IPlatformUtilsService _platformUtilsService;
private string _email;
private string _masterPassword;
private bool _showPassword;
private bool _acceptPolicies;
private bool _checkExposedMasterPassword;
public RegisterPageViewModel()
{
@@ -32,12 +40,15 @@ namespace Bit.App.Pages
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
_auditService = ServiceContainer.Resolve<IAuditService>();
PageTitle = AppResources.CreateAccount;
TogglePasswordCommand = new Command(TogglePassword);
ToggleConfirmPasswordCommand = new Command(ToggleConfirmPassword);
SubmitCommand = new Command(async () => await SubmitAsync());
ShowTerms = !_platformUtilsService.IsSelfHost();
PasswordStrengthViewModel = new PasswordStrengthViewModel(this);
CheckExposedMasterPassword = true;
}
public ICommand PoliciesClickCommand => new Command<string>((url) =>
@@ -61,6 +72,34 @@ namespace Bit.App.Pages
get => _acceptPolicies;
set => SetProperty(ref _acceptPolicies, value);
}
public bool CheckExposedMasterPassword
{
get => _checkExposedMasterPassword;
set => SetProperty(ref _checkExposedMasterPassword, value);
}
public string MasterPassword
{
get => _masterPassword;
set
{
SetProperty(ref _masterPassword, value);
PasswordStrengthViewModel.CalculatePasswordStrength();
}
}
public string Email
{
get => _email;
set => SetProperty(ref _email, value);
}
public string Password => MasterPassword;
public List<string> UserInputs => PasswordStrengthViewModel.GetPasswordStrengthUserInput(Email);
public string MasterPasswordMininumCharactersDescription => string.Format(AppResources.YourMasterPasswordCannotBeRecoveredIfYouForgetItXCharactersMinimum,
Constants.MasterPasswordMinimumChars);
public PasswordStrengthViewModel PasswordStrengthViewModel { get; }
public bool ShowTerms { get; set; }
public Command SubmitCommand { get; }
public Command TogglePasswordCommand { get; }
@@ -68,13 +107,10 @@ namespace Bit.App.Pages
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string Name { get; set; }
public string Email { get; set; }
public string MasterPassword { get; set; }
public string ConfirmMasterPassword { get; set; }
public string Hint { get; set; }
public Action RegistrationSuccess { get; set; }
public Action CloseAction { get; set; }
protected override II18nService i18nService => _i18nService;
protected override IEnvironmentService environmentService => _environmentService;
protected override IDeviceActionService deviceActionService => _deviceActionService;
@@ -110,7 +146,7 @@ namespace Bit.App.Pages
AppResources.Ok);
return;
}
if (MasterPassword.Length < 8)
if (MasterPassword.Length < Constants.MasterPasswordMinimumChars)
{
await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordLengthValMessage,
AppResources.AnErrorHasOccurred, AppResources.Ok);
@@ -128,8 +164,10 @@ namespace Bit.App.Pages
AppResources.AnErrorHasOccurred, AppResources.Ok);
return;
}
// TODO: Password strength check?
if (await IsPasswordWeakOrExposed())
{
return;
}
if (showLoading)
{
@@ -138,9 +176,8 @@ namespace Bit.App.Pages
Name = string.IsNullOrWhiteSpace(Name) ? null : Name;
Email = Email.Trim().ToLower();
var kdf = KdfType.PBKDF2_SHA256;
var kdfIterations = 100_000;
var key = await _cryptoService.MakeKeyAsync(MasterPassword, Email, kdf, kdfIterations);
var kdfConfig = new KdfConfig(KdfType.PBKDF2_SHA256, Constants.Pbkdf2Iterations, null, null);
var key = await _cryptoService.MakeKeyAsync(MasterPassword, Email, kdfConfig);
var encKey = await _cryptoService.MakeEncKeyAsync(key);
var hashedPassword = await _cryptoService.HashPasswordAsync(MasterPassword, key);
var keys = await _cryptoService.MakeKeyPairAsync(encKey.Item1);
@@ -151,8 +188,10 @@ namespace Bit.App.Pages
MasterPasswordHash = hashedPassword,
MasterPasswordHint = Hint,
Key = encKey.Item2.EncryptedString,
Kdf = kdf,
KdfIterations = kdfIterations,
Kdf = kdfConfig.Type,
KdfIterations = kdfConfig.Iterations,
KdfMemory = kdfConfig.Memory,
KdfParallelism = kdfConfig.Parallelism,
Keys = new KeysRequest
{
PublicKey = keys.Item1,
@@ -160,6 +199,7 @@ namespace Bit.App.Pages
},
CaptchaResponse = _captchaToken,
};
// TODO: org invite?
try
@@ -208,5 +248,43 @@ namespace Bit.App.Pages
entry.Focus();
entry.CursorPosition = String.IsNullOrEmpty(ConfirmMasterPassword) ? 0 : ConfirmMasterPassword.Length;
}
private async Task<bool> IsPasswordWeakOrExposed()
{
try
{
var title = string.Empty;
var message = string.Empty;
var exposedPassword = CheckExposedMasterPassword ? await _auditService.PasswordLeakedAsync(MasterPassword) > 0 : false;
var weakPassword = PasswordStrengthViewModel.PasswordStrengthLevel <= PasswordStrengthLevel.Weak;
if (exposedPassword && weakPassword)
{
title = AppResources.WeakAndExposedMasterPassword;
message = AppResources.WeakPasswordIdentifiedAndFoundInADataBreachAlertDescription;
}
else if (exposedPassword)
{
title = AppResources.ExposedMasterPassword;
message = AppResources.PasswordFoundInADataBreachAlertDescription;
}
else if (weakPassword)
{
title = AppResources.WeakMasterPassword;
message = AppResources.WeakPasswordIdentifiedUseAStrongPasswordToProtectYourAccount;
}
if (exposedPassword || weakPassword)
{
return !await _platformUtilsService.ShowDialogAsync(message, title, AppResources.Yes, AppResources.No);
}
}
catch (Exception ex)
{
HandleException(ex);
}
return false;
}
}
}

View File

@@ -47,7 +47,7 @@
Margin="0, 12, 0, 0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
BorderColor="{DynamicResource PrimaryColor}">
<Label
Text="{u:I18n ResetPasswordAutoEnrollInviteWarning}"
StyleClass="text-muted, text-sm, text-bold"
@@ -69,7 +69,7 @@
Margin="0, 12, 0, 0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
BorderColor="{DynamicResource PrimaryColor}">
<Label
Text="{Binding PolicySummary}"
StyleClass="text-muted, text-sm, text-bold"

View File

@@ -5,6 +5,7 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
@@ -137,8 +138,8 @@ namespace Bit.App.Pages
}
if (IsPolicyInEffect)
{
var userInput = await GetPasswordStrengthUserInput();
var passwordStrength = _passwordGenerationService.PasswordStrength(MasterPassword, userInput);
var userInputs = _passwordGenerationService.GetPasswordStrengthUserInput(await _stateService.GetEmailAsync());
var passwordStrength = _passwordGenerationService.PasswordStrength(MasterPassword, userInputs);
if (!await _policyService.EvaluateMasterPassword(passwordStrength.Score, MasterPassword, Policy))
{
await Page.DisplayAlert(AppResources.MasterPasswordPolicyValidationTitle,
@@ -148,7 +149,7 @@ namespace Bit.App.Pages
}
else
{
if (MasterPassword.Length < 8)
if (MasterPassword.Length < Constants.MasterPasswordMinimumChars)
{
await Page.DisplayAlert(AppResources.MasterPasswordPolicyValidationTitle,
AppResources.MasterPasswordLengthValMessage, AppResources.Ok);
@@ -162,10 +163,9 @@ namespace Bit.App.Pages
return;
}
var kdf = KdfType.PBKDF2_SHA256;
var kdfIterations = 100000;
var kdfConfig = new KdfConfig(KdfType.PBKDF2_SHA256, Constants.Pbkdf2Iterations, null, null);
var email = await _stateService.GetEmailAsync();
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdf, kdfIterations);
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdfConfig);
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.ServerAuthorization);
var localMasterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.LocalAuthorization);
@@ -186,8 +186,10 @@ namespace Bit.App.Pages
MasterPasswordHash = masterPasswordHash,
Key = encKey.Item2.EncryptedString,
MasterPasswordHint = Hint,
Kdf = kdf,
KdfIterations = kdfIterations,
Kdf = kdfConfig.Type.GetValueOrDefault(KdfType.PBKDF2_SHA256),
KdfIterations = kdfConfig.Iterations.GetValueOrDefault(Constants.Pbkdf2Iterations),
KdfMemory = kdfConfig.Memory,
KdfParallelism = kdfConfig.Parallelism,
OrgIdentifier = OrgIdentifier,
Keys = new KeysRequest
{
@@ -201,8 +203,7 @@ namespace Bit.App.Pages
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
// Set Password and relevant information
await _apiService.SetPasswordAsync(request);
await _stateService.SetKdfTypeAsync(kdf);
await _stateService.SetKdfIterationsAsync(kdfIterations);
await _stateService.SetKdfConfigurationAsync(kdfConfig);
await _cryptoService.SetKeyAsync(key);
await _cryptoService.SetKeyHashAsync(localMasterPasswordHash);
await _cryptoService.SetEncKeyAsync(encKey.Item2.EncryptedString);

View File

@@ -44,7 +44,7 @@
Margin="0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
BorderColor="{DynamicResource PrimaryColor}">
<Label
Text="{u:I18n UpdateMasterPasswordWarning}"
StyleClass="text-muted, text-sm, text-bold"
@@ -67,7 +67,7 @@
Margin="0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
BorderColor="{DynamicResource PrimaryColor}">
<Label
Text="{Binding PolicySummary}"
StyleClass="text-muted, text-sm, text-bold"

View File

@@ -43,12 +43,11 @@ namespace Bit.App.Pages
}
// Retrieve details for key generation
var kdf = await _stateService.GetKdfTypeAsync();
var kdfIterations = await _stateService.GetKdfIterationsAsync();
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
var email = await _stateService.GetEmailAsync();
// Create new key and hash new password
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdf, kdfIterations);
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdfConfig);
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key);
// Create new encKey for the User

View File

@@ -129,7 +129,8 @@ namespace Bit.App.Pages
{
if (useCurrentActiveAccount)
{
return new AvatarImageSource(await _stateService.GetNameAsync(), await _stateService.GetEmailAsync());
var user = await _stateService.GetActiveUserCustomDataAsync(a => (a?.Profile?.UserId, a?.Profile?.Name, a?.Profile?.Email, a?.Profile?.AvatarColor));
return new AvatarImageSource(user.UserId, user.Name, user.Email, user.AvatarColor);
}
return new AvatarImageSource();
}

View File

@@ -1,4 +1,10 @@
using Bit.App.Controls;
using System;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.Forms;
@@ -8,6 +14,9 @@ namespace Bit.App.Pages
{
private string _pageTitle = string.Empty;
private AvatarImageSource _avatar;
private LazyResolve<IDeviceActionService> _deviceActionService = new LazyResolve<IDeviceActionService>();
private LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
private LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
public string PageTitle
{
@@ -22,5 +31,21 @@ namespace Bit.App.Pages
}
public ContentPage Page { get; set; }
protected void HandleException(Exception ex, string message = null)
{
if (ex is ApiException apiException && apiException.Error != null)
{
message = apiException.Error.GetSingleMessage();
}
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>
{
await _deviceActionService.Value.HideLoadingAsync();
await _platformUtilsService.Value.ShowDialogAsync(message ?? AppResources.GenericErrorMessage);
}).FireAndForget();
_logger.Value.Exception(ex);
}
}
}

View File

@@ -16,6 +16,22 @@ namespace Bit.App.Pages
protected abstract IPlatformUtilsService platformUtilsService { get; }
protected string _captchaToken = null;
protected async Task<bool> HandleCaptchaAsync(string captchaSiteKey, bool needsCaptcha, Func<Task> onSuccess)
{
if (!needsCaptcha)
{
_captchaToken = null;
return false;
}
if (await HandleCaptchaAsync(captchaSiteKey))
{
await onSuccess();
_captchaToken = null;
}
return true;
}
protected async Task<bool> HandleCaptchaAsync(string CaptchaSiteKey)
{
var callbackUri = "bitwarden://captcha-callback";

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8" ?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
@@ -20,8 +20,9 @@
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:LocalizableEnumConverter x:Key="localizableEnum" />
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
<xct:EnumToBoolConverter x:Key="enumToBool"/>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
<ToolbarItem Text="{u:I18n Cancel}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"
x:Name="_closeItem" x:Key="closeItem" />
<ToolbarItem Text="{u:I18n Select}"
Clicked="Select_Clicked"
@@ -66,7 +67,7 @@
Margin="0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
BorderColor="{DynamicResource PrimaryColor}">
<Label
Text="{u:I18n PasswordGeneratorPolicyInEffect}"
StyleClass="text-muted, text-sm, text-bold"
@@ -251,7 +252,7 @@
Grid.Row="1"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowAnonAddyHiddenValueIcon}"
Text="{Binding ShowAnonAddyApiAccessToken, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
@@ -280,7 +281,7 @@
IsPassword="{Binding ShowFirefoxRelayApiAccessToken, Converter={StaticResource inverseBool}}"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowFirefoxRelayHiddenValueIcon}"
Text="{Binding ShowFirefoxRelayApiAccessToken, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
@@ -301,7 +302,49 @@
IsPassword="{Binding ShowSimpleLoginApiKey, Converter={StaticResource inverseBool}}"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowSimpleLoginHiddenValueIcon}"
Text="{Binding ShowSimpleLoginApiKey, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
</Grid>
<!--DUCKDUCKGO OPTIONS-->
<Grid StyleClass="box-row, box-row-input"
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.DuckDuckGo}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Text="{u:I18n APIKeyRequiredParenthesis}"
StyleClass="box-label"/>
<Entry
x:Name="_duckDuckGoApiAccessTokenEntry"
Text="{Binding DuckDuckGoApiKey}"
StyleClass="box-value"
Grid.Row="1"
IsPassword="{Binding ShowDuckDuckGoApiKey, Converter={StaticResource inverseBool}}"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowDuckDuckGoApiKey, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
</Grid>
<!--FASTMAIL OPTIONS-->
<Grid StyleClass="box-row, box-row-input"
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.Fastmail}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Text="{u:I18n APIKeyRequiredParenthesis}"
StyleClass="box-label"/>
<Entry
x:Name="_fastmailApiAccessTokenEntry"
Text="{Binding FastmailApiKey}"
StyleClass="box-value"
Grid.Row="1"
IsPassword="{Binding ShowFastmailApiKey, Converter={StaticResource inverseBool}}"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowFastmailApiKey, Converter={StaticResource iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
@@ -462,7 +505,7 @@
StyleClass="box-value"
HorizontalOptions="End"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n LowercaseAtoZ}" />
AutomationProperties.Name="{u:I18n LowercaseAtoZ}"/>
</StackLayout>
<BoxView StyleClass="box-row-separator" />
<StackLayout StyleClass="box-row, box-row-switch">

View File

@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.App.Styles;
using Bit.Core.Abstractions;
@@ -18,11 +19,11 @@ namespace Bit.App.Pages
private readonly Action<string> _selectAction;
private readonly TabsPage _tabsPage;
public GeneratorPage(bool fromTabPage, Action<string> selectAction = null, TabsPage tabsPage = null, bool isUsernameGenerator = false, string emailWebsite = null, bool editMode = false)
public GeneratorPage(bool fromTabPage, Action<string> selectAction = null, TabsPage tabsPage = null, bool isUsernameGenerator = false, string emailWebsite = null, bool editMode = false, AppOptions appOptions = null)
{
_tabsPage = tabsPage;
InitializeComponent();
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
_vm = BindingContext as GeneratorPageViewModel;
_vm.Page = this;
_fromTabPage = fromTabPage;
@@ -31,6 +32,7 @@ namespace Bit.App.Pages
_vm.IsUsername = isUsernameGenerator;
_vm.EmailWebsite = emailWebsite;
_vm.EditMode = editMode;
_vm.IosExtension = appOptions?.IosExtension ?? false;
var isIos = Device.RuntimePlatform == Device.iOS;
if (selectAction != null)
{
@@ -134,14 +136,6 @@ namespace Bit.App.Pages
await _vm.SliderChangedAsync();
}
private async void Close_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await Navigation.PopModalAsync();
}
}
public override async Task UpdateOnThemeChanged()
{
await base.UpdateOnThemeChanged();

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
@@ -21,6 +22,7 @@ namespace Bit.App.Pages
private readonly IClipboardService _clipboardService;
private readonly IUsernameGenerationService _usernameGenerationService;
private readonly ITokenService _tokenService;
private readonly IDeviceActionService _deviceActionService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private PasswordGenerationOptions _options;
@@ -50,6 +52,8 @@ namespace Bit.App.Pages
private bool _showFirefoxRelayApiAccessToken;
private bool _showAnonAddyApiAccessToken;
private bool _showSimpleLoginApiKey;
private bool _showDuckDuckGoApiKey;
private bool _showFastmailApiKey;
private bool _editMode;
public GeneratorPageViewModel()
@@ -59,6 +63,7 @@ namespace Bit.App.Pages
_clipboardService = ServiceContainer.Resolve<IClipboardService>();
_usernameGenerationService = ServiceContainer.Resolve<IUsernameGenerationService>();
_tokenService = ServiceContainer.Resolve<ITokenService>();
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
PageTitle = AppResources.Generator;
GeneratorTypeOptions = new List<GeneratorType> {
@@ -76,6 +81,8 @@ namespace Bit.App.Pages
ForwardedEmailServiceTypeOptions = new List<ForwardedEmailServiceType> {
ForwardedEmailServiceType.AnonAddy,
ForwardedEmailServiceType.DuckDuckGo,
ForwardedEmailServiceType.Fastmail,
ForwardedEmailServiceType.FirefoxRelay,
ForwardedEmailServiceType.SimpleLogin
};
@@ -89,8 +96,9 @@ namespace Bit.App.Pages
UsernameTypePromptHelpCommand = new Command(UsernameTypePromptHelp);
RegenerateCommand = new AsyncCommand(RegenerateAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
RegenerateUsernameCommand = new AsyncCommand(RegenerateUsernameAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
ToggleForwardedEmailHiddenValueCommand = new AsyncCommand(ToggleForwardedEmailHiddenValueAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
CopyCommand = new AsyncCommand(CopyAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
ToggleForwardedEmailHiddenValueCommand = new AsyncCommand(ToggleForwardedEmailHiddenValueAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
CopyCommand = new AsyncCommand(CopyAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
CloseCommand = new AsyncCommand(CloseAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
}
public List<GeneratorType> GeneratorTypeOptions { get; set; }
@@ -104,6 +112,7 @@ namespace Bit.App.Pages
public ICommand RegenerateUsernameCommand { get; set; }
public ICommand ToggleForwardedEmailHiddenValueCommand { get; set; }
public ICommand CopyCommand { get; set; }
public ICommand CloseCommand { get; set; }
public string Password
{
@@ -140,6 +149,8 @@ namespace Bit.App.Pages
set => SetProperty(ref _isUsername, value);
}
public bool IosExtension { get; set; }
public bool ShowTypePicker
{
get => _showTypePicker;
@@ -454,15 +465,9 @@ namespace Bit.App.Pages
{
return _showAnonAddyApiAccessToken;
}
set => SetProperty(ref _showAnonAddyApiAccessToken, value,
additionalPropertyNames: new string[]
{
nameof(ShowAnonAddyHiddenValueIcon)
});
set => SetProperty(ref _showAnonAddyApiAccessToken, value);
}
public string ShowAnonAddyHiddenValueIcon => _showAnonAddyApiAccessToken ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string AnonAddyDomainName
{
get => _usernameOptions.AnonAddyDomainName;
@@ -497,15 +502,9 @@ namespace Bit.App.Pages
{
return _showFirefoxRelayApiAccessToken;
}
set => SetProperty(ref _showFirefoxRelayApiAccessToken, value,
additionalPropertyNames: new string[]
{
nameof(ShowFirefoxRelayHiddenValueIcon)
});
set => SetProperty(ref _showFirefoxRelayApiAccessToken, value);
}
public string ShowFirefoxRelayHiddenValueIcon => _showFirefoxRelayApiAccessToken ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string SimpleLoginApiKey
{
get => _usernameOptions.SimpleLoginApiKey;
@@ -526,14 +525,55 @@ namespace Bit.App.Pages
{
return _showSimpleLoginApiKey;
}
set => SetProperty(ref _showSimpleLoginApiKey, value,
additionalPropertyNames: new string[]
{
nameof(ShowSimpleLoginHiddenValueIcon)
});
set => SetProperty(ref _showSimpleLoginApiKey, value);
}
public string ShowSimpleLoginHiddenValueIcon => _showSimpleLoginApiKey ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string DuckDuckGoApiKey
{
get => _usernameOptions.DuckDuckGoApiKey;
set
{
if (_usernameOptions.DuckDuckGoApiKey != value)
{
_usernameOptions.DuckDuckGoApiKey = value;
TriggerPropertyChanged(nameof(DuckDuckGoApiKey));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowDuckDuckGoApiKey
{
get
{
return _showDuckDuckGoApiKey;
}
set => SetProperty(ref _showDuckDuckGoApiKey, value);
}
public string FastmailApiKey
{
get => _usernameOptions.FastMailApiKey;
set
{
if (_usernameOptions.FastMailApiKey != value)
{
_usernameOptions.FastMailApiKey = value;
TriggerPropertyChanged(nameof(FastmailApiKey));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowFastmailApiKey
{
get
{
return _showFastmailApiKey;
}
set => SetProperty(ref _showFastmailApiKey, value);
}
public bool CapitalizeRandomWordUsername
{
@@ -606,6 +646,7 @@ namespace Bit.App.Pages
LoadFromOptions();
_usernameOptions = await _usernameGenerationService.GetOptionsAsync();
await _tokenService.PrepareTokenForDecodingAsync();
_usernameOptions.PlusAddressedEmail = _tokenService.GetEmail();
_usernameOptions.EmailWebsite = EmailWebsite;
_usernameOptions.CatchAllEmailType = _usernameOptions.PlusAddressedEmailType = string.IsNullOrWhiteSpace(EmailWebsite) || !EditMode ? UsernameEmailType.Random : UsernameEmailType.Website;
@@ -681,6 +722,7 @@ namespace Bit.App.Pages
return;
}
_usernameOptions.EmailWebsite = EmailWebsite;
await _usernameGenerationService.SaveOptionsAsync(_usernameOptions);
if (regenerate && UsernameTypeSelected != UsernameType.ForwardedEmailAlias)
@@ -729,6 +771,18 @@ namespace Bit.App.Pages
}
}
public async Task CloseAsync()
{
if (IosExtension)
{
_deviceActionService.CloseExtensionPopUp();
}
else
{
await Page.Navigation.PopModalAsync();
}
}
private void LoadFromOptions()
{
AllowAmbiguousChars = _options.AllowAmbiguousChar.GetValueOrDefault();
@@ -757,6 +811,8 @@ namespace Bit.App.Pages
TriggerPropertyChanged(nameof(FirefoxRelayApiAccessToken));
TriggerPropertyChanged(nameof(AnonAddyDomainName));
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken));
TriggerPropertyChanged(nameof(DuckDuckGoApiKey));
TriggerPropertyChanged(nameof(FastmailApiKey));
TriggerPropertyChanged(nameof(CatchAllEmailDomain));
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
TriggerPropertyChanged(nameof(UsernameTypeSelected));
@@ -765,6 +821,7 @@ namespace Bit.App.Pages
TriggerPropertyChanged(nameof(PlusAddressedEmail));
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
TriggerPropertyChanged(nameof(UsernameTypeDescriptionLabel));
TriggerPropertyChanged(nameof(EmailWebsite));
}
private void SetOptions()
@@ -827,6 +884,12 @@ namespace Bit.App.Pages
case ForwardedEmailServiceType.SimpleLogin:
ShowSimpleLoginApiKey = !ShowSimpleLoginApiKey;
break;
case ForwardedEmailServiceType.DuckDuckGo:
ShowDuckDuckGoApiKey = !ShowDuckDuckGoApiKey;
break;
case ForwardedEmailServiceType.Fastmail:
ShowFastmailApiKey = !ShowFastmailApiKey;
break;
}
}
}

View File

@@ -67,7 +67,7 @@
Margin="0, 12, 0, 0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
BorderColor="{DynamicResource PrimaryColor}">
<Label
Text="{u:I18n SendDisabledWarning}"
StyleClass="text-muted, text-sm, text-bold"
@@ -79,7 +79,7 @@
Margin="0, 12, 0, 0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
BorderColor="{DynamicResource PrimaryColor}">
<Label
Text="{u:I18n SendOptionsPolicyInEffect}"
StyleClass="text-muted, text-sm, text-bold"

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