mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
Compare commits
328 Commits
fedemkr-pa
...
vault/pm-7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f312e8c4d2 | ||
|
|
1b3d5e5eb2 | ||
|
|
81fbb91c76 | ||
|
|
45641aadfe | ||
|
|
27380abd89 | ||
|
|
9db32ca019 | ||
|
|
1fd7dd462e | ||
|
|
f04ff7777a | ||
|
|
64775694e0 | ||
|
|
3c0007a21a | ||
|
|
ff49d041be | ||
|
|
b931263662 | ||
|
|
3a10e09469 | ||
|
|
ebc068d820 | ||
|
|
6bec0ede05 | ||
|
|
35ff235010 | ||
|
|
01bd5a7b8d | ||
|
|
3fce8c76bc | ||
|
|
3b64d7b979 | ||
|
|
f343a2cdbb | ||
|
|
39da2a82c6 | ||
|
|
970d3c2621 | ||
|
|
faa515b415 | ||
|
|
74085689d3 | ||
|
|
9a9fb85ad8 | ||
|
|
e7f9d64edb | ||
|
|
144fc7c727 | ||
|
|
53aedea93a | ||
|
|
459d20c019 | ||
|
|
dd997aaa47 | ||
|
|
a8529fa4b7 | ||
|
|
d1e82c9f1d | ||
|
|
46c1d72b3c | ||
|
|
9bc2901255 | ||
|
|
01fe329f3b | ||
|
|
67f7b3156e | ||
|
|
e3441845cd | ||
|
|
3f463647a0 | ||
|
|
4f169a6fe3 | ||
|
|
82c2e91446 | ||
|
|
39187732c0 | ||
|
|
7482808857 | ||
|
|
4292542155 | ||
|
|
e41abf5003 | ||
|
|
fd233fa27f | ||
|
|
4c2932f4d0 | ||
|
|
a10481603d | ||
|
|
19f238d9bb | ||
|
|
6f6487ccc9 | ||
|
|
b8ff0e0244 | ||
|
|
dd3dc82595 | ||
|
|
40c80f082d | ||
|
|
85755902e1 | ||
|
|
bca5b95446 | ||
|
|
602627b5fa | ||
|
|
6f32afb919 | ||
|
|
2ca47a4da4 | ||
|
|
38d3a7ed41 | ||
|
|
4ff56ba11e | ||
|
|
22d0cc681c | ||
|
|
4e0a18cce5 | ||
|
|
c9fdfa7a15 | ||
|
|
850a7e754a | ||
|
|
18fae7ddd8 | ||
|
|
67c5f79625 | ||
|
|
04e7cfe06d | ||
|
|
d6c2ebe4c2 | ||
|
|
2a28294f91 | ||
|
|
b83473ce3a | ||
|
|
8584bbaecc | ||
|
|
2f3cded9c5 | ||
|
|
eff0ea7ce7 | ||
|
|
6c3a53dd76 | ||
|
|
e34a58e875 | ||
|
|
9f92fdeb29 | ||
|
|
cf8d801c55 | ||
|
|
eaa6844742 | ||
|
|
29e2f728e0 | ||
|
|
c31444dc8b | ||
|
|
16e1b60a4d | ||
|
|
fe160a570f | ||
|
|
71de3bedf4 | ||
|
|
a508bea4b0 | ||
|
|
a73923c4f7 | ||
|
|
11465e8975 | ||
|
|
4c88524f0e | ||
|
|
f1c20e03bc | ||
|
|
920a2273c5 | ||
|
|
d339514d9a | ||
|
|
75ec96f282 | ||
|
|
96a9978ef8 | ||
|
|
1ae388cb03 | ||
|
|
c6aaf5002f | ||
|
|
75be6504e1 | ||
|
|
cae1825e3f | ||
|
|
c23100d281 | ||
|
|
c9c0d0b4d6 | ||
|
|
fec0743e4d | ||
|
|
8f8a5795d3 | ||
|
|
4631a9e62c | ||
|
|
51ee6a84b5 | ||
|
|
8d5006c0bd | ||
|
|
37208571fe | ||
|
|
759627b3c7 | ||
|
|
08fac4752f | ||
|
|
9307e7e0d8 | ||
|
|
b1a0801f9b | ||
|
|
04cc53b934 | ||
|
|
c138658a31 | ||
|
|
f1854f2c04 | ||
|
|
e4056d9ee6 | ||
|
|
eb95a54db2 | ||
|
|
7ddea4c70b | ||
|
|
3804e86995 | ||
|
|
b23bed182f | ||
|
|
f8e421871b | ||
|
|
d0103496b9 | ||
|
|
cd8952221e | ||
|
|
155c7539bd | ||
|
|
5f43681fb1 | ||
|
|
d2965e6e10 | ||
|
|
ec1ade7761 | ||
|
|
f35bef0d7b | ||
|
|
138d37cf5e | ||
|
|
fc2fed079f | ||
|
|
9c441a98f4 | ||
|
|
1491872b62 | ||
|
|
c74636ffa5 | ||
|
|
05677f93c5 | ||
|
|
0aef241df6 | ||
|
|
e0b58461b5 | ||
|
|
cd33c7f608 | ||
|
|
9d29af36e5 | ||
|
|
4472d7f9a8 | ||
|
|
999579915c | ||
|
|
63904fd303 | ||
|
|
2cb6872e4e | ||
|
|
f539bf051d | ||
|
|
14f845d623 | ||
|
|
133a80acef | ||
|
|
b43790de9a | ||
|
|
0bdd63df06 | ||
|
|
c6544b49e9 | ||
|
|
8e1a8b5f0e | ||
|
|
4717f5e230 | ||
|
|
01ee1ff845 | ||
|
|
75b4655f38 | ||
|
|
9b2f596d15 | ||
|
|
55fb71744d | ||
|
|
ee252be634 | ||
|
|
66f0471f2e | ||
|
|
6b9eeba88d | ||
|
|
0a1fbfafb5 | ||
|
|
0a5d772886 | ||
|
|
70c8a264d2 | ||
|
|
b5fbb2cade | ||
|
|
9027755b71 | ||
|
|
6d625f285b | ||
|
|
822ad7564e | ||
|
|
1949a450fd | ||
|
|
27fa79e0bd | ||
|
|
1e29eacc61 | ||
|
|
b81d26d589 | ||
|
|
cd107b6161 | ||
|
|
7ac3646fb0 | ||
|
|
d1e4e8645a | ||
|
|
36a648e53e | ||
|
|
6c04ac67b1 | ||
|
|
dfb7a0621f | ||
|
|
1eb9e5f8ea | ||
|
|
b149e7549c | ||
|
|
e3877cc589 | ||
|
|
275ae76761 | ||
|
|
a1e4f0aaa2 | ||
|
|
adaef0d15b | ||
|
|
fa4a2247e3 | ||
|
|
5d2fc4530f | ||
|
|
9b64af3423 | ||
|
|
b6ff6e34f6 | ||
|
|
6d4c706026 | ||
|
|
14fd026ea0 | ||
|
|
a4392a8730 | ||
|
|
1b885ea438 | ||
|
|
fa022a1a4f | ||
|
|
6011b63958 | ||
|
|
7d79b98bf2 | ||
|
|
d4e75e9de8 | ||
|
|
c3370b58ec | ||
|
|
3de13325c9 | ||
|
|
c253c110c1 | ||
|
|
bf35d1f2dc | ||
|
|
05b6aa90b6 | ||
|
|
e39898bba6 | ||
|
|
da0866cc85 | ||
|
|
b3140381ab | ||
|
|
c01a8f8d93 | ||
|
|
8484b4af30 | ||
|
|
6b9faed45f | ||
|
|
770a1c5dfe | ||
|
|
3c87d4db1c | ||
|
|
8b3c6ab35f | ||
|
|
90912977c4 | ||
|
|
740b368b8c | ||
|
|
3a40a4cda8 | ||
|
|
9bcd2e51f7 | ||
|
|
741214a1cc | ||
|
|
aad87dfdce | ||
|
|
8fc1e9a3b9 | ||
|
|
2a8e15146e | ||
|
|
8559d5908e | ||
|
|
f60c4d94fe | ||
|
|
05858bea48 | ||
|
|
5cbef47fd4 | ||
|
|
4bf695d18c | ||
|
|
c24e0dfa28 | ||
|
|
9ccd0834ff | ||
|
|
a806f17d3b | ||
|
|
436a162df2 | ||
|
|
f2c298607e | ||
|
|
b5dbb9ae5e | ||
|
|
7a5f7c0274 | ||
|
|
17acb57732 | ||
|
|
5803635f44 | ||
|
|
19c393842f | ||
|
|
15a306490d | ||
|
|
a4a3d31c19 | ||
|
|
922dc683af | ||
|
|
bae1b3e891 | ||
|
|
a5888827c9 | ||
|
|
0348940a12 | ||
|
|
4c2998337d | ||
|
|
7ea86380f4 | ||
|
|
406f4425c8 | ||
|
|
95ca911444 | ||
|
|
fa62510e09 | ||
|
|
65dc73495d | ||
|
|
02a2e41118 | ||
|
|
bd6f8295e7 | ||
|
|
0a0cb7093b | ||
|
|
465e5eff76 | ||
|
|
5b756aaf7a | ||
|
|
d168a7b750 | ||
|
|
7f4bbafe3c | ||
|
|
a5804df6a3 | ||
|
|
bfa2a51608 | ||
|
|
32be08daae | ||
|
|
0a628cc8a8 | ||
|
|
80c424ed03 | ||
|
|
99fb5463cf | ||
|
|
c5d941e1df | ||
|
|
3edfef6169 | ||
|
|
1c8742511a | ||
|
|
8e424d6c05 | ||
|
|
390c303b90 | ||
|
|
443f7282b8 | ||
|
|
50109ee70b | ||
|
|
9ffdfd51cc | ||
|
|
04e409f3c6 | ||
|
|
6c143bad57 | ||
|
|
286e18059a | ||
|
|
553bf9ed0a | ||
|
|
ddb27b52d3 | ||
|
|
6c504aa710 | ||
|
|
62254aef8d | ||
|
|
06a0195a6d | ||
|
|
df2b0b21d5 | ||
|
|
e6b1bab860 | ||
|
|
ce41eb0578 | ||
|
|
1a0b52d644 | ||
|
|
16ada4993c | ||
|
|
3795f3aa17 | ||
|
|
eceb506c77 | ||
|
|
2c7870d660 | ||
|
|
f02b3415a3 | ||
|
|
beda4e9ff8 | ||
|
|
df4d89cd52 | ||
|
|
5f12bb9747 | ||
|
|
5712639492 | ||
|
|
e0a3c301fb | ||
|
|
27306fe353 | ||
|
|
a31f15559f | ||
|
|
0e75f3f5c8 | ||
|
|
363da063fa | ||
|
|
974a571455 | ||
|
|
e0c721098c | ||
|
|
a86f6e3034 | ||
|
|
fe17288b99 | ||
|
|
7324da9d47 | ||
|
|
69aa6fc044 | ||
|
|
e840dc2e30 | ||
|
|
eb25ee5d1b | ||
|
|
840f24dbe5 | ||
|
|
c6309173ba | ||
|
|
946c465f0c | ||
|
|
e90409d842 | ||
|
|
484b5a5160 | ||
|
|
2688209752 | ||
|
|
53e0e55915 | ||
|
|
ca57948d9f | ||
|
|
aaf082faba | ||
|
|
e7aeb08cae | ||
|
|
f177968958 | ||
|
|
f1d59210f9 | ||
|
|
62213c0aaf | ||
|
|
8be8abb8fe | ||
|
|
174acbc558 | ||
|
|
4bcc7c0d71 | ||
|
|
14b2960f30 | ||
|
|
455c3a257c | ||
|
|
8c623a2067 | ||
|
|
3cdf1c2f0e | ||
|
|
ce9503fa0c | ||
|
|
2e4da1b87d | ||
|
|
d63a219272 | ||
|
|
c92cd90a97 | ||
|
|
1dcd3a3daa | ||
|
|
efb8763d3c | ||
|
|
90649d1c8b | ||
|
|
828055791f | ||
|
|
87eebda55f | ||
|
|
7542d1ae1c | ||
|
|
990de4ea4e | ||
|
|
0dbc23f734 | ||
|
|
9f6c8601d3 | ||
|
|
8b7f9b9fb3 | ||
|
|
d17789d5ee | ||
|
|
b8f0747dd4 | ||
|
|
8ef9443b1e |
10
.github/CODEOWNERS
vendored
10
.github/CODEOWNERS
vendored
@@ -11,11 +11,11 @@
|
||||
.github/workflows @bitwarden/dept-devops
|
||||
|
||||
# DevOps for Version Bumping
|
||||
src/Android/Properties/AndroidManifest.xml
|
||||
src/App/Platforms/Android/AndroidManifest.xml
|
||||
src/iOS.Autofill/Info.plist
|
||||
src/iOS.Extension/Info.plist
|
||||
src/iOS.ShareExtension/Info.plist
|
||||
src/iOS/Info.plist
|
||||
src/App/Platforms/iOS/Info.plist
|
||||
|
||||
## Auth team files ##
|
||||
|
||||
@@ -30,14 +30,14 @@ src/watchOS @bitwarden/team-vault-dev
|
||||
src/Core/Services/EmailForwarders @bitwarden/team-tools-dev
|
||||
|
||||
## Crowdin Sync files ##
|
||||
src/App/Resources @bitwarden/team-tools-dev
|
||||
src/Core/Resources/Localization @bitwarden/team-tools-dev
|
||||
src/watchOS/bitwarden/bitwarden\ WatchKit\ Extension/Localization @bitwarden/team-tools-dev
|
||||
store/apple @bitwarden/team-tools-dev
|
||||
store/google @bitwarden/team-tools-dev
|
||||
|
||||
## Locales ##
|
||||
src/App/Resources/AppResources.Designer.cs
|
||||
src/App/Resources/AppResources.resx
|
||||
src/Core/Resources/Localization/AppResources.Designer.cs
|
||||
src/Core/Resources/Localization/AppResources.resx
|
||||
src/watchOS/bitwarden/bitwarden\ WatchKit\ Extension/Localization/en.lproj
|
||||
store/apple/en
|
||||
store/google/en
|
||||
|
||||
BIN
.github/secrets/GoogleService-Info.plist.gpg
vendored
BIN
.github/secrets/GoogleService-Info.plist.gpg
vendored
Binary file not shown.
BIN
.github/secrets/app_fdroid-keystore.jks.gpg
vendored
BIN
.github/secrets/app_fdroid-keystore.jks.gpg
vendored
Binary file not shown.
BIN
.github/secrets/app_play-keystore.jks.gpg
vendored
BIN
.github/secrets/app_play-keystore.jks.gpg
vendored
Binary file not shown.
BIN
.github/secrets/app_upload-keystore.jks.gpg
vendored
BIN
.github/secrets/app_upload-keystore.jks.gpg
vendored
Binary file not shown.
BIN
.github/secrets/bitwarden-mobile-key.p12.gpg
vendored
BIN
.github/secrets/bitwarden-mobile-key.p12.gpg
vendored
Binary file not shown.
BIN
.github/secrets/dist_autofill.mobileprovision.gpg
vendored
BIN
.github/secrets/dist_autofill.mobileprovision.gpg
vendored
Binary file not shown.
BIN
.github/secrets/dist_bitwarden.mobileprovision.gpg
vendored
BIN
.github/secrets/dist_bitwarden.mobileprovision.gpg
vendored
Binary file not shown.
BIN
.github/secrets/dist_extension.mobileprovision.gpg
vendored
BIN
.github/secrets/dist_extension.mobileprovision.gpg
vendored
Binary file not shown.
Binary file not shown.
BIN
.github/secrets/dist_watch_app.mobileprovision.gpg
vendored
BIN
.github/secrets/dist_watch_app.mobileprovision.gpg
vendored
Binary file not shown.
Binary file not shown.
3
.github/secrets/google-services.json.gpg
vendored
3
.github/secrets/google-services.json.gpg
vendored
@@ -1,3 +0,0 @@
|
||||
<EFBFBD>
|
||||
K<>Y#<23>(<28><><EFBFBD><EFBFBD>EI߄T?)l<><6C><EFBFBD><18><><10>"=<3D>|<7C>'e<><0E>m<EFBFBD>/~<7E><>'F<><46>><3E><><EFBFBD><EFBFBD>l<EFBFBD>b<EFBFBD>[<5B>+R<><52>iL<69><4C>"<22><><EFBFBD>~V:<3A><>p<EFBFBD>a<17>ڵel%8t<38><74>튖<EFBFBD>y<<3C>n<EFBFBD><6E><EFBFBD>aU<61>w<16>JD<4A><44><1F><>We<57>9<EFBFBD><39><EFBFBD><EFBFBD><x8d<38>O<EFBFBD>j\<14>ד<EFBFBD><D793><EFBFBD>Vq<56><71>
|
||||
Ǻ<EFBFBD>-<2D>#<23><><11><>]$<24>(<28>l,<2C>Br<42><02><>d<><64><EFBFBD>a-<2D><><EFBFBD>:<3A><>:<3A><04>9b,!Em<02><19><>Qf<>D<EFBFBD>g<EFBFBD><06><0E>x(P<>ȡ~<7E><EFBFBD><CDB9> <09><>[<06><>!:<3A>;f<><66>
|
||||
BIN
.github/secrets/iphone-distribution-cert.p12.gpg
vendored
BIN
.github/secrets/iphone-distribution-cert.p12.gpg
vendored
Binary file not shown.
BIN
.github/secrets/play_creds.json.gpg
vendored
BIN
.github/secrets/play_creds.json.gpg
vendored
Binary file not shown.
BIN
.github/secrets/store_fdroid-keystore.jks.gpg
vendored
BIN
.github/secrets/store_fdroid-keystore.jks.gpg
vendored
Binary file not shown.
5
.github/workflows/build-beta.yml
vendored
Normal file
5
.github/workflows/build-beta.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
name: Build Beta
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
409
.github/workflows/build.yml
vendored
409
.github/workflows/build.yml
vendored
@@ -31,6 +31,7 @@ jobs:
|
||||
- name: Print lines of code
|
||||
run: cloc --vcs git --exclude-dir Resources,store,test,Properties --include-lang C#,XAML
|
||||
|
||||
|
||||
setup:
|
||||
name: Setup
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -58,6 +59,7 @@ jobs:
|
||||
echo "hotfix_branch_exists=0" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
|
||||
android:
|
||||
name: Android
|
||||
runs-on: windows-2022
|
||||
@@ -67,7 +69,8 @@ jobs:
|
||||
matrix:
|
||||
variant: ["prod", "qa"]
|
||||
env:
|
||||
android_folder_path: src/App/Platforms/Android
|
||||
android_folder_path: src\App\Platforms\Android
|
||||
android_folder_path_bash: src/App/Platforms/Android
|
||||
steps:
|
||||
- name: Setup NuGet
|
||||
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
|
||||
@@ -82,7 +85,7 @@ jobs:
|
||||
- name: Set up MSBuild
|
||||
uses: microsoft/setup-msbuild@ede762b26a2de8d110bb5a3db4d7e0e080c0e917 # v1.3.3
|
||||
|
||||
# This step might be obsolete at some point as .NET MAUI workloads
|
||||
# This step might be obsolete at some point as .NET MAUI workloads
|
||||
# are starting to come pre-installed on the GH Actions build agents.
|
||||
- name: Install MAUI Workload
|
||||
run: dotnet workload install maui --ignore-failed-sources
|
||||
@@ -93,7 +96,8 @@ jobs:
|
||||
- name: Install Microsoft OpenJDK 11
|
||||
run: |
|
||||
choco install microsoft-openjdk11 --no-progress
|
||||
Write-Output "JAVA_HOME=$(Get-ChildItem -Path 'C:\Program Files\Microsoft\jdk*' | Select -First 1 -ExpandProperty FullName)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
Write-Output "JAVA_HOME=$(Get-ChildItem -Path 'C:\Program Files\Microsoft\jdk*' | `
|
||||
Select -First 1 -ExpandProperty FullName)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
Write-Output "Java Home: $env:JAVA_HOME"
|
||||
|
||||
- name: Print environment
|
||||
@@ -109,39 +113,43 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Decrypt secrets
|
||||
env:
|
||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||
run: |
|
||||
mkdir -p ~/secrets
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output ./${{ env.main_app_folder_path }}/app_play-keystore.jks ./.github/secrets/app_play-keystore.jks.gpg
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output ./${{ env.main_app_folder_path }}/app_upload-keystore.jks ./.github/secrets/app_upload-keystore.jks.gpg
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output $HOME/secrets/play_creds.json ./.github/secrets/play_creds.json.gpg
|
||||
- name: Download secrets
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
run: |
|
||||
mkdir -p $HOME/secrets
|
||||
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name app_play-keystore.jks --file ./${{ env.android_folder_path_bash }}/app_play-keystore.jks --output none
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name app_upload-keystore.jks --file ./${{ env.android_folder_path_bash }}/app_upload-keystore.jks --output none
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name play_creds.json --file $HOME/secrets/play_creds.json --output none
|
||||
shell: bash
|
||||
|
||||
- name: Decrypt secrets - Google Services
|
||||
- name: Download secrets - Google Services
|
||||
if: ${{ matrix.variant == 'prod' }}
|
||||
env:
|
||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
run: |
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output ./${{ env.android_folder_path }}/google-services.json ./.github/secrets/google-services.json.gpg
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name google-services.json --file ./${{ env.android_folder_path_bash }}/google-services.json --output none
|
||||
shell: bash
|
||||
|
||||
- name: Increment version
|
||||
run: |
|
||||
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
|
||||
echo "##### Setting Android Version Code to $BUILD_NUMBER" | tee -a $GITHUB_STEP_SUMMARY
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Setting Version Code $BUILD_NUMBER"
|
||||
echo "########################################"
|
||||
|
||||
sed -i "s/android:versionCode=\"1\"/android:versionCode=\"$BUILD_NUMBER\"/" \
|
||||
./${{ env.android_folder_path }}/AndroidManifest.xml
|
||||
./${{ env.android_folder_path_bash }}/AndroidManifest.xml
|
||||
shell: bash
|
||||
|
||||
- name: Restore packages
|
||||
@@ -150,83 +158,75 @@ jobs:
|
||||
- name: Restore tools
|
||||
run: dotnet tool restore
|
||||
|
||||
# - name: Verify Format
|
||||
# run: dotnet tool run dotnet-format --check
|
||||
# - name: Run Core tests
|
||||
# run: |
|
||||
# dotnet test test/Core.Test/Core.Test.csproj --logger "trx;LogFileName=test-results.trx" `
|
||||
# /p:CustomConstants=UT
|
||||
|
||||
- name: Run Core tests
|
||||
run: dotnet test test/Core.Test/Core.Test.csproj --logger "trx;LogFileName=test-results.trx" /p:CustomConstants=UT
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@eaa763f6ffc21c7a37837f56cd5f9737f27fc6c8 # v1.8.0
|
||||
if: always()
|
||||
with:
|
||||
name: Test Results
|
||||
path: "**/test-results.trx"
|
||||
reporter: dotnet-trx
|
||||
fail-on-error: true
|
||||
# - name: Report test results
|
||||
# uses: dorny/test-reporter@eaa763f6ffc21c7a37837f56cd5f9737f27fc6c8 # v1.8.0
|
||||
# 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
|
||||
run: dotnet build .\store\google\Publisher\Publisher.csproj /p:Configuration=Release
|
||||
|
||||
- name: Setup Android build (${{ matrix.variant }})
|
||||
run: dotnet cake build.cake --target Android --variant ${{ matrix.variant }}
|
||||
|
||||
- name: Build Android
|
||||
run: |
|
||||
$configuration = "Release";
|
||||
$projToBuild = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}");
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Build $configuration Configuration"
|
||||
Write-Output "########################################"
|
||||
|
||||
dotnet build $projToBuild -c $configuration -f ${{ env.target-net-version }}-android
|
||||
|
||||
- name: Sign Android Build
|
||||
- name: Build & Sign Android
|
||||
env:
|
||||
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
|
||||
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
|
||||
run: |
|
||||
$projToBuild = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}");
|
||||
$projToBuild = "$($env:GITHUB_WORKSPACE)/${{ env.main_app_project_path }}";
|
||||
$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 "########################################"
|
||||
|
||||
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android /p:AndroidPackageFormats=aab /p:AndroidKeyStore=true /p:AndroidSigningKeyStore=$("app_upload-keystore.jks") /p:AndroidSigningKeyAlias=upload /p:AndroidSigningKeyPass="$($env:UPLOAD_KEYSTORE_PASSWORD)" /p:AndroidSigningStorePass="$($env:UPLOAD_KEYSTORE_PASSWORD)" --no-restore
|
||||
$signingUploadKeyStore = "$($env:GITHUB_WORKSPACE)\${{ env.android_folder_path }}\app_upload-keystore.jks"
|
||||
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android `
|
||||
/p:AndroidPackageFormats=aab `
|
||||
/p:AndroidKeyStore=true `
|
||||
/p:AndroidSigningKeyStore=$signingUploadKeyStore `
|
||||
/p:AndroidSigningKeyAlias=upload `
|
||||
/p:AndroidSigningKeyPass="$($env:UPLOAD_KEYSTORE_PASSWORD)" `
|
||||
/p:AndroidSigningStorePass="$($env:UPLOAD_KEYSTORE_PASSWORD)" --no-restore
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Copy Google Play Bundle to project root"
|
||||
Write-Output "########################################"
|
||||
|
||||
$signedAabPath = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_folder_path }}/bin/Release/${{ env.target-net-version }}-android/publish/$($packageName)-Signed.aab");
|
||||
$signedAabDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).aab");
|
||||
$signedAabPath = "$($env:GITHUB_WORKSPACE)\${{ env.main_app_folder_path }}\bin\Release\${{ env.target-net-version }}-android\publish\$($packageName)-Signed.aab";
|
||||
$signedAabDestPath = "$($env:GITHUB_WORKSPACE)\$($packageName).aab";
|
||||
Copy-Item $signedAabPath $signedAabDestPath
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Sign APK Release Configuration"
|
||||
Write-Output "########################################"
|
||||
|
||||
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android /p:AndroidKeyStore=true /p:AndroidSigningKeyStore=$("app_play-keystore.jks") /p:AndroidSigningKeyAlias=bitwarden /p:AndroidSigningKeyPass="$($env:PLAY_KEYSTORE_PASSWORD)" /p:AndroidSigningStorePass="$($env:PLAY_KEYSTORE_PASSWORD)" --no-restore
|
||||
$signingPlayKeyStore = "$($env:GITHUB_WORKSPACE)\${{ env.android_folder_path }}\app_play-keystore.jks"
|
||||
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android `
|
||||
/p:AndroidKeyStore=true `
|
||||
/p:AndroidSigningKeyStore=$signingPlayKeyStore `
|
||||
/p:AndroidSigningKeyAlias=bitwarden `
|
||||
/p:AndroidSigningKeyPass="$($env:PLAY_KEYSTORE_PASSWORD)" `
|
||||
/p:AndroidSigningStorePass="$($env:PLAY_KEYSTORE_PASSWORD)" --no-restore
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Copy Release APK to project root"
|
||||
Write-Output "########################################"
|
||||
|
||||
$signedApkPath = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_folder_path }}/bin/Release/${{ env.target-net-version }}-android/publish/$($packageName)-Signed.apk");
|
||||
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).apk");
|
||||
|
||||
$signedApkPath = "$($env:GITHUB_WORKSPACE)\${{ env.main_app_folder_path }}\bin\Release\${{ env.target-net-version }}-android\publish\$($packageName)-Signed.apk";
|
||||
$signedApkDestPath = "$($env:GITHUB_WORKSPACE)\$($packageName).apk";
|
||||
Copy-Item $signedApkPath $signedApkDestPath
|
||||
|
||||
- name: Upload Prod .aab artifact
|
||||
if: ${{ matrix.variant == 'prod' }}
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab
|
||||
path: ./com.x8bit.bitwarden.aab
|
||||
@@ -234,7 +234,7 @@ jobs:
|
||||
|
||||
- name: Upload Prod .apk artifact
|
||||
if: ${{ matrix.variant == 'prod' }}
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk
|
||||
path: ./com.x8bit.bitwarden.apk
|
||||
@@ -242,7 +242,7 @@ jobs:
|
||||
|
||||
- name: Upload Other .apk artifact
|
||||
if: ${{ matrix.variant != 'prod' }}
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||
@@ -262,7 +262,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk sha file for prod
|
||||
if: ${{ matrix.variant == 'prod' }}
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: bw-android-apk-sha256.txt
|
||||
path: ./bw-android-apk-sha256.txt
|
||||
@@ -270,7 +270,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk sha file for other
|
||||
if: ${{ matrix.variant != 'prod' }}
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||
path: ./bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||
@@ -283,20 +283,20 @@ jobs:
|
||||
|| (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/net7.0/Publisher.dll"
|
||||
CREDS_PATH="$HOME/secrets/play_creds.json"
|
||||
AAB_PATH="$GITHUB_WORKSPACE/com.x8bit.bitwarden.aab"
|
||||
TRACK="internal"
|
||||
$publisherPath = "$($env:GITHUB_WORKSPACE)\store\google\Publisher\bin\Release\net8.0\Publisher.dll"
|
||||
$credsPath = "$($HOME)\secrets\play_creds.json"
|
||||
$aabPath = "$($env:GITHUB_WORKSPACE)\com.x8bit.bitwarden.aab"
|
||||
$track = "internal"
|
||||
|
||||
dotnet $PUBLISHER_PATH $CREDS_PATH $AAB_PATH $TRACK
|
||||
shell: bash
|
||||
dotnet $publisherPath $credsPath $aabPath $track
|
||||
|
||||
|
||||
f-droid:
|
||||
name: F-Droid Build
|
||||
runs-on: windows-2022
|
||||
env:
|
||||
android_folder_path: src/App/Platforms/Android
|
||||
android_folder_path: src\App\Platforms\Android
|
||||
android_folder_path_bash: src/App/Platforms/Android
|
||||
android_manifest_path: src/App/Platforms/Android/AndroidManifest.xml
|
||||
steps:
|
||||
- name: Setup NuGet
|
||||
@@ -312,7 +312,7 @@ jobs:
|
||||
- name: Set up MSBuild
|
||||
uses: microsoft/setup-msbuild@ede762b26a2de8d110bb5a3db4d7e0e080c0e917 # v1.3.3
|
||||
|
||||
# This step might be obsolete at some point as .NET MAUI workloads
|
||||
# This step might be obsolete at some point as .NET MAUI workloads
|
||||
# are starting to come pre-installed on the GH Actions build agents.
|
||||
- name: Install MAUI Workload
|
||||
run: dotnet workload install maui --ignore-failed-sources
|
||||
@@ -337,23 +337,25 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Decrypt secrets
|
||||
env:
|
||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||
run: |
|
||||
mkdir -p ~/secrets
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output ./${{ env.main_app_folder_path }}/app_fdroid-keystore.jks ./.github/secrets/app_fdroid-keystore.jks.gpg
|
||||
- name: Download secrets
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
FILE: app_fdroid-keystore.jks
|
||||
run: |
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME --name $FILE \
|
||||
--file ${{ env.android_folder_path_bash }}/$FILE --output none
|
||||
shell: bash
|
||||
|
||||
- name: Increment version
|
||||
run: |
|
||||
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Setting Version Code $BUILD_NUMBER"
|
||||
echo "########################################"
|
||||
echo "##### Setting F-Droid Version Code to $BUILD_NUMBER" | tee -a $GITHUB_STEP_SUMMARY
|
||||
|
||||
sed -i "s/android:versionCode=\"1\"/android:versionCode=\"$BUILD_NUMBER\"/" \
|
||||
./${{ env.android_manifest_path }}
|
||||
@@ -361,21 +363,16 @@ jobs:
|
||||
|
||||
- name: Clean for F-Droid
|
||||
run: |
|
||||
$appPath = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}");
|
||||
$corePath = $($env:GITHUB_WORKSPACE + "/src/Core/Core.csproj");
|
||||
$directoryBuildProps = $($env:GITHUB_WORKSPACE + "/Directory.Build.props");
|
||||
|
||||
$androidManifest = $($env:GITHUB_WORKSPACE + "/${{ env.android_manifest_path }}");
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Backup project files"
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Back up project files"
|
||||
|
||||
Copy-Item $androidManifest $($androidManifest + ".original");
|
||||
Copy-Item $appPath $($appPath + ".original");
|
||||
Copy-Item $directoryBuildProps $($directoryBuildProps + ".original");
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Cleanup Android Manifest"
|
||||
Write-Output "########################################"
|
||||
|
||||
$xml=New-Object XML;
|
||||
$xml.Load($androidManifest);
|
||||
@@ -385,44 +382,39 @@ jobs:
|
||||
|
||||
$xml.Save($androidManifest);
|
||||
|
||||
Write-Output "##### Enabling FDROID constant"
|
||||
|
||||
(Get-Content $directoryBuildProps).Replace('<!-- <CustomConstants>FDROID</CustomConstants> -->', '<CustomConstants>FDROID</CustomConstants>') | Set-Content $directoryBuildProps
|
||||
|
||||
- name: Restore packages
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build for F-Droid
|
||||
run: |
|
||||
$configuration = "Release";
|
||||
$projToBuild = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}");
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Build $configuration FDROID
|
||||
Write-Output "########################################"
|
||||
|
||||
dotnet build $projToBuild -c $configuration -f ${{ env.target-net-version }}-android /p:CustomConstants="FDROID"
|
||||
|
||||
- name: Sign for F-Droid
|
||||
- name: Build & Sign F-Droid
|
||||
env:
|
||||
FDROID_KEYSTORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }}
|
||||
run: |
|
||||
$projToBuild = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}");
|
||||
$projToBuild = "$($env:GITHUB_WORKSPACE)\${{ env.main_app_project_path }}";
|
||||
$packageName = "com.x8bit.bitwarden";
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Sign FDroid"
|
||||
Write-Output "########################################"
|
||||
|
||||
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android /p:AndroidKeyStore=true /p:AndroidSigningKeyStore=$("app_fdroid-keystore.jks") /p:AndroidSigningKeyAlias=bitwarden /p:AndroidSigningKeyPass="$($env:FDROID_KEYSTORE_PASSWORD)" /p:AndroidSigningStorePass="$($env:FDROID_KEYSTORE_PASSWORD)" /p:CustomConstants="FDROID" --no-restore
|
||||
$signingFdroidKeyStore = "$($env:GITHUB_WORKSPACE)\${{ env.android_folder_path }}\app_fdroid-keystore.jks"
|
||||
dotnet build $projToBuild -c Release -f ${{ env.target-net-version }}-android `
|
||||
/p:AndroidKeyStore=true `
|
||||
/p:AndroidSigningKeyStore=$signingFdroidKeyStore `
|
||||
/p:AndroidSigningKeyAlias=bitwarden `
|
||||
/p:AndroidSigningKeyPass="$($env:FDROID_KEYSTORE_PASSWORD)" `
|
||||
/p:AndroidSigningStorePass="$($env:FDROID_KEYSTORE_PASSWORD)" ` --no-restore
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Copy FDroid apk to project root"
|
||||
Write-Output "########################################"
|
||||
|
||||
$signedApkPath = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_folder_path }}/bin/Release/${{ env.target-net-version }}-android/publish/$($packageName)-Signed.apk");
|
||||
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden-fdroid.apk");
|
||||
$signedApkPath = "$($env:GITHUB_WORKSPACE)\${{ env.main_app_folder_path }}\bin\Release\${{ env.target-net-version }}-android\$($packageName)-Signed.apk";
|
||||
$signedApkDestPath = "$($env:GITHUB_WORKSPACE)\com.x8bit.bitwarden-fdroid.apk";
|
||||
|
||||
Copy-Item $signedApkPath $signedApkDestPath
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
path: ./com.x8bit.bitwarden-fdroid.apk
|
||||
@@ -434,12 +426,13 @@ jobs:
|
||||
-t sha256 | Out-File -Encoding ASCII ./bw-fdroid-apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid sha file
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
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-13
|
||||
@@ -458,13 +451,13 @@ jobs:
|
||||
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
|
||||
with:
|
||||
nuget-version: 6.4.0
|
||||
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
# This step might be obsolete at some point as .NET MAUI workloads
|
||||
|
||||
# This step might be obsolete at some point as .NET MAUI workloads
|
||||
# are starting to come pre-installed on the GH Actions build agents.
|
||||
- name: Install MAUI Workload
|
||||
run: dotnet workload install maui --ignore-failed-sources
|
||||
@@ -493,73 +486,71 @@ jobs:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "appcenter-ios-token"
|
||||
|
||||
- name: Decrypt secrets
|
||||
- name: Download Provisioning Profiles secrets
|
||||
env:
|
||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: profiles
|
||||
run: |
|
||||
mkdir -p ~/secrets
|
||||
mkdir -p $HOME/secrets
|
||||
profiles=(
|
||||
"dist_autofill.mobileprovision"
|
||||
"dist_bitwarden.mobileprovision"
|
||||
"dist_extension.mobileprovision"
|
||||
"dist_share_extension.mobileprovision"
|
||||
"dist_bitwarden_watch_app.mobileprovision"
|
||||
"dist_bitwarden_watch_app_extension.mobileprovision"
|
||||
)
|
||||
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output $HOME/secrets/bitwarden-mobile-key.p12 ./.github/secrets/bitwarden-mobile-key.p12.gpg
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output $HOME/secrets/iphone-distribution-cert.p12 ./.github/secrets/iphone-distribution-cert.p12.gpg
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output $HOME/secrets/dist_autofill.mobileprovision ./.github/secrets/dist_autofill.mobileprovision.gpg
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output $HOME/secrets/dist_bitwarden.mobileprovision ./.github/secrets/dist_bitwarden.mobileprovision.gpg
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output $HOME/secrets/dist_extension.mobileprovision ./.github/secrets/dist_extension.mobileprovision.gpg
|
||||
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
|
||||
for FILE in "${profiles[@]}"
|
||||
do
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME --name $FILE \
|
||||
--file $HOME/secrets/$FILE --output none
|
||||
done
|
||||
|
||||
- name: Download Google Services secret
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
FILE: GoogleService-Info.plist
|
||||
run: |
|
||||
mkdir -p $HOME/secrets
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME --name $FILE \
|
||||
--file src/watchOS/bitwarden/$FILE --output none
|
||||
|
||||
- name: Increment version
|
||||
run: |
|
||||
BUILD_NUMBER=$((100 + $GITHUB_RUN_NUMBER))
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Setting CFBundleVersion $BUILD_NUMBER"
|
||||
echo "########################################"
|
||||
|
||||
echo "### CFBundleVersion $BUILD_NUMBER" >> $GITHUB_STEP_SUMMARY
|
||||
echo "##### Setting iOS CFBundleVersion to $BUILD_NUMBER" | tee -a $GITHUB_STEP_SUMMARY
|
||||
|
||||
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./${{ env.ios_folder_path }}/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.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
|
||||
agvtool new-version -all $BUILD_NUMBER
|
||||
|
||||
- name: Update Entitlements
|
||||
run: |
|
||||
echo "########################################"
|
||||
echo "##### Updating Entitlements"
|
||||
echo "########################################"
|
||||
|
||||
perl -0777 -pi.bak -e 's/<key>aps-environment<\/key>\s*<string>development<\/string>/<key>aps-environment<\/key>\n\t<string>production<\/string>/' ./${{ env.ios_folder_path }}/Entitlements.plist
|
||||
|
||||
|
||||
- name: Get certificates
|
||||
run: |
|
||||
mkdir -p $HOME/certificates
|
||||
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/ios-distribution |
|
||||
jq -r .value | base64 -d > $HOME/certificates/ios-distribution.p12
|
||||
|
||||
- name: Set up Keychain
|
||||
env:
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
|
||||
MOBILE_KEY_PASSWORD: ${{ secrets.IOS_KEY_PASSWORD }}
|
||||
DIST_CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
|
||||
run: |
|
||||
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
||||
security set-keychain-settings -lut 1200 build.keychain
|
||||
security import ~/secrets/bitwarden-mobile-key.p12 -k build.keychain -P $MOBILE_KEY_PASSWORD \
|
||||
-T /usr/bin/codesign -T /usr/bin/security
|
||||
security import ~/secrets/iphone-distribution-cert.p12 -k build.keychain -P $DIST_CERT_PASSWORD \
|
||||
-T /usr/bin/codesign -T /usr/bin/security
|
||||
|
||||
security import $HOME/certificates/ios-distribution.p12 -k build.keychain -P "" -T /usr/bin/codesign \
|
||||
-T /usr/bin/security
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
|
||||
|
||||
- name: Set up provisioning profiles
|
||||
@@ -568,8 +559,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
|
||||
WATCH_APP_PROFILE_PATH=$HOME/secrets/dist_bitwarden_watch_app.mobileprovision
|
||||
WATCH_APP_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_bitwarden_watch_app_extension.mobileprovision
|
||||
PROFILES_DIR_PATH=$HOME/Library/MobileDevice/Provisioning\ Profiles
|
||||
|
||||
mkdir -p "$PROFILES_DIR_PATH"
|
||||
@@ -597,74 +588,50 @@ jobs:
|
||||
|
||||
- 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 "########################################"
|
||||
|
||||
- name: Archive Build for App Store
|
||||
run: |
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Archive for Release ios-arm64
|
||||
Write-Output "########################################"
|
||||
|
||||
echo "##### Archive for Release ios-arm64"
|
||||
dotnet publish ${{ env.main_app_project_path }} -c Release -f ${{ env.target-net-version }}-ios /p:RuntimeIdentifier=ios-arm64 /p:ArchiveOnBuild=true /p:MtouchUseLlvm=false
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Done"
|
||||
Write-Output "########################################"
|
||||
shell: pwsh
|
||||
|
||||
- name: Archive Build for Mobile Automation
|
||||
run: |
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Archive Debug for iossimulator-x64
|
||||
Write-Output "########################################"
|
||||
|
||||
echo "##### Archive Debug for iossimulator-x64"
|
||||
dotnet build ${{ env.main_app_project_path }} -c Debug -f ${{ env.target-net-version }}-ios /p:RuntimeIdentifier=iossimulator-x64 /p:ArchiveOnBuild=true /p:MtouchUseLlvm=false
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Done"
|
||||
Write-Output "########################################"
|
||||
ls ~/Library/Developer/Xcode/Archives
|
||||
shell: pwsh
|
||||
ls $HOME/Library/Developer/Xcode/Archives
|
||||
|
||||
- name: Export .ipa for App Store
|
||||
env:
|
||||
EXPORT_OPTIONS_PATH: ./.github/resources/export-options-app-store.plist
|
||||
EXPORT_PATH: ./bitwarden-export
|
||||
run: |
|
||||
EXPORT_OPTIONS_PATH="./.github/resources/export-options-app-store.plist"
|
||||
ARCHIVE_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive"
|
||||
EXPORT_PATH="./bitwarden-export"
|
||||
|
||||
xcodebuild -exportArchive -archivePath $ARCHIVE_PATH -exportPath $EXPORT_PATH \
|
||||
-exportOptionsPlist $EXPORT_OPTIONS_PATH
|
||||
|
||||
- name: Export .app for Automation CI
|
||||
env:
|
||||
ARCHIVE_PATH: ./${{ env.main_app_folder_path }}/bin/Debug/${{ env.target-net-version }}-ios/iossimulator-x64
|
||||
EXPORT_PATH: ./bitwarden-export
|
||||
run: |
|
||||
ARCHIVE_PATH="./${{ env.main_app_folder_path }}/bin/Debug/${{ env.target-net-version }}-ios/iossimulator-x64"
|
||||
EXPORT_PATH="./bitwarden-export"
|
||||
|
||||
zip -r -q ${{ env.app_ci_output_filename }}.app.zip $ARCHIVE_PATH
|
||||
mv ${{ env.app_ci_output_filename }}.app.zip $EXPORT_PATH
|
||||
|
||||
- name: Copy all dSYMs files to upload
|
||||
env:
|
||||
EXPORT_PATH: ./bitwarden-export
|
||||
WATCH_ARCHIVE_DSYMS_PATH: ./src/watchOS/bitwarden.xcarchive/dSYMs/
|
||||
WATCH_DSYMS_EXPORT_PATH: ./bitwarden-export/Watch_dSYMs
|
||||
run: |
|
||||
ARCHIVE_DSYMS_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive/dSYMs"
|
||||
EXPORT_PATH="./bitwarden-export"
|
||||
|
||||
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
|
||||
|
||||
- name: Upload App Store .ipa & dSYMs artifacts
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: Bitwarden iOS
|
||||
path: |
|
||||
@@ -673,7 +640,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .app file for Automation CI
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: ${{ env.app_ci_output_filename }}.app.zip
|
||||
path: ./bitwarden-export/${{ env.app_ci_output_filename }}.app.zip
|
||||
@@ -707,10 +674,7 @@ jobs:
|
||||
|| (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" \;
|
||||
|
||||
- name: Validate app in App Store
|
||||
@@ -726,7 +690,6 @@ jobs:
|
||||
run: |
|
||||
xcrun altool --validate-app --type ios --file "./bitwarden-export/Bitwarden.ipa" \
|
||||
--username "$APPLE_ID_USERNAME" --password "$APPLE_ID_PASSWORD"
|
||||
shell: bash
|
||||
|
||||
- name: Deploy to App Store
|
||||
if: |
|
||||
@@ -770,13 +733,13 @@ jobs:
|
||||
secrets: "crowdin-api-token"
|
||||
|
||||
- name: Upload Sources
|
||||
uses: crowdin/github-action@97bef4fd3f1b853eb105bc99b8d0d563760e024c # v1.17.0
|
||||
uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d # v1.19.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
with:
|
||||
config: crowdin.yml
|
||||
crowdin_branch_name: main
|
||||
crowdin_branch_name: main
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
@@ -794,27 +757,11 @@ jobs:
|
||||
steps:
|
||||
- name: Check if any job failed
|
||||
if: |
|
||||
(github.ref == 'refs/heads/main')
|
||||
|| (github.ref == 'refs/heads/rc')
|
||||
|| (github.ref == 'refs/heads/hotfix-rc')
|
||||
env:
|
||||
CLOC_STATUS: ${{ needs.cloc.result }}
|
||||
ANDROID_STATUS: ${{ needs.android.result }}
|
||||
F_DROID_STATUS: ${{ needs.f-droid.result }}
|
||||
IOS_STATUS: ${{ needs.ios.result }}
|
||||
CROWDIN_PUSH_STATUS: ${{ needs.crowdin-push.result }}
|
||||
run: |
|
||||
if [ "$CLOC_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$ANDROID_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$F_DROID_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$IOS_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$CROWDIN_PUSH_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
fi
|
||||
(github.ref == 'refs/heads/main'
|
||||
|| github.ref == 'refs/heads/rc'
|
||||
|| github.ref == 'refs/heads/hotfix-rc')
|
||||
&& contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
|
||||
53
.github/workflows/cleanup-rc-branch.yml
vendored
Normal file
53
.github/workflows/cleanup-rc-branch.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: Cleanup RC Branch
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
|
||||
jobs:
|
||||
delete-rc:
|
||||
name: Delete RC Branch
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve bot secrets
|
||||
id: retrieve-bot-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: bitwarden-ci
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
|
||||
- name: Check if a RC branch exists
|
||||
id: branch-check
|
||||
run: |
|
||||
hotfix_rc_branch_check=$(git ls-remote --heads origin hotfix-rc | wc -l)
|
||||
rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
|
||||
|
||||
if [[ "${hotfix_rc_branch_check}" -gt 0 ]]; then
|
||||
echo "hotfix-rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
|
||||
echo "name=hotfix-rc" >> $GITHUB_OUTPUT
|
||||
elif [[ "${rc_branch_check}" -gt 0 ]]; then
|
||||
echo "rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
|
||||
echo "name=rc" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Delete RC branch
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.branch-check.outputs.name }}
|
||||
run: |
|
||||
if ! [[ -z "$BRANCH_NAME" ]]; then
|
||||
git push --quiet origin --delete $BRANCH_NAME
|
||||
echo "Deleted $BRANCH_NAME branch." | tee -a $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
2
.github/workflows/crowdin-pull.yml
vendored
2
.github/workflows/crowdin-pull.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@97bef4fd3f1b853eb105bc99b8d0d563760e024c # v1.17.0
|
||||
uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d # v1.19.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
58
.github/workflows/release.yml
vendored
58
.github/workflows/release.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
branch-name: ${{ steps.branch.outputs.branch-name }}
|
||||
steps:
|
||||
- name: Branch check
|
||||
if: github.event.inputs.release_type != 'Dry Run'
|
||||
if: inputs.release_type != 'Dry Run'
|
||||
run: |
|
||||
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
|
||||
echo "==================================="
|
||||
@@ -44,9 +44,9 @@ jobs:
|
||||
id: version
|
||||
uses: bitwarden/gh-actions/release-version-check@main
|
||||
with:
|
||||
release-type: ${{ github.event.inputs.release_type }}
|
||||
release-type: ${{ inputs.release_type }}
|
||||
project-type: xamarin
|
||||
file: src/Android/Properties/AndroidManifest.xml
|
||||
file: src/App/Platforms/Android/AndroidManifest.xml
|
||||
|
||||
- name: Get branch name
|
||||
id: branch
|
||||
@@ -55,8 +55,8 @@ jobs:
|
||||
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create GitHub deployment
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: chrnorm/deployment-action@d42cde7132fcec920de534fffc3be83794335c00 # v2.0.5
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7
|
||||
id: deployment
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
@@ -67,16 +67,16 @@ jobs:
|
||||
|
||||
|
||||
- name: Download all artifacts
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ steps.branch.outputs.branch-name }}
|
||||
|
||||
- name: Dry Run - Download all artifacts
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0
|
||||
if: ${{ inputs.release_type == 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
@@ -86,8 +86,8 @@ jobs:
|
||||
run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS
|
||||
|
||||
- name: Create release
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: ncipollo/release-action@6c75be85e571768fa31b40abf38de58ba0397db5 # v1.13.0
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0
|
||||
with:
|
||||
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
|
||||
./com.x8bit.bitwarden.apk/com.x8bit.bitwarden.apk,
|
||||
@@ -103,16 +103,16 @@ jobs:
|
||||
draft: true
|
||||
|
||||
- name: Update deployment status to Success
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }}
|
||||
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
||||
if: ${{ inputs.release_type != 'Dry Run' && success() }}
|
||||
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
state: 'success'
|
||||
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
|
||||
- name: Update deployment status to Failure
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }}
|
||||
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
||||
if: ${{ inputs.release_type != 'Dry Run' && failure() }}
|
||||
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
state: 'failure'
|
||||
@@ -129,8 +129,8 @@ jobs:
|
||||
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- name: Download F-Droid .apk artifact
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
@@ -138,8 +138,8 @@ jobs:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
|
||||
- name: Dry Run - Download F-Droid .apk artifact
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0
|
||||
if: ${{ inputs.release_type == 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: '16.x'
|
||||
|
||||
@@ -176,13 +176,19 @@ jobs:
|
||||
- name: Install Node dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Decrypt secrets
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Download secrets
|
||||
env:
|
||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
run: |
|
||||
mkdir -p ~/secrets
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output ./store/fdroid/keystore.jks ./.github/secrets/store_fdroid-keystore.jks.gpg
|
||||
mkdir -p $HOME/secrets
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name store_fdroid-keystore.jks --file ./store/fdroid/keystore.jks --output none
|
||||
|
||||
- name: Compile for F-Droid Store
|
||||
env:
|
||||
@@ -211,5 +217,5 @@ jobs:
|
||||
cd $GITHUB_WORKSPACE
|
||||
|
||||
- name: Deploy to gh-pages
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
run: npm run deploy
|
||||
|
||||
24
.github/workflows/version-auto-bump.yml
vendored
24
.github/workflows/version-auto-bump.yml
vendored
@@ -11,24 +11,6 @@ jobs:
|
||||
name: Bump Mobile Version
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Calculate bumped version
|
||||
id: version
|
||||
env:
|
||||
RELEASE_TAG: ${{ github.ref }}
|
||||
run: |
|
||||
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"
|
||||
|
||||
NEW_PATCH=$((CURR_PATCH+1))
|
||||
NEW_VER=$CURR_MAJOR.$NEW_PATCH
|
||||
echo "New Version: $NEW_VER"
|
||||
echo "new_version=$NEW_VER" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
@@ -41,9 +23,9 @@ jobs:
|
||||
keyvault: bitwarden-ci
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
- name: "Bump version to ${{ steps.version.outputs.new_version }}"
|
||||
- name: Trigger Version Bump workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
run: |
|
||||
echo '{"cut_rc_branch": "false", "version_number": "${{ steps.version.outputs.new_version }}"}' | \
|
||||
gh workflow run version-bump.yml --json --repo bitwarden/mobile
|
||||
echo '{"cut_rc_branch": "false"}' | \
|
||||
gh workflow run version-bump.yml --json --repo bitwarden/mobile
|
||||
|
||||
174
.github/workflows/version-bump.yml
vendored
174
.github/workflows/version-bump.yml
vendored
@@ -1,13 +1,13 @@
|
||||
---
|
||||
name: Version Bump
|
||||
run-name: Version Bump - v${{ inputs.version_number }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_number:
|
||||
description: "New version (example: '2024.1.0')"
|
||||
required: true
|
||||
version_number_override:
|
||||
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
|
||||
required: false
|
||||
type: string
|
||||
cut_rc_branch:
|
||||
description: "Cut RC branch?"
|
||||
default: true
|
||||
@@ -15,22 +15,16 @@ on:
|
||||
|
||||
jobs:
|
||||
bump_version:
|
||||
name: "Bump Version to v${{ inputs.version_number }}"
|
||||
name: Bump Version
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
version: ${{ steps.set-final-version-output.outputs.version }}
|
||||
steps:
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
- name: Validate version input
|
||||
if: ${{ inputs.version_number_override != '' }}
|
||||
uses: bitwarden/gh-actions/version-check@main
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-gpg-private-key,
|
||||
github-gpg-private-key-passphrase,
|
||||
github-pat-bitwarden-devops-bot-repo-scope"
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
@@ -47,6 +41,20 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-gpg-private-key,
|
||||
github-gpg-private-key-passphrase,
|
||||
github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
|
||||
with:
|
||||
@@ -55,25 +63,38 @@ jobs:
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||
git config --local user.name "bitwarden-devops-bot"
|
||||
|
||||
- name: Create Version Branch
|
||||
id: create-branch
|
||||
run: |
|
||||
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }}
|
||||
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
|
||||
git switch -c $NAME
|
||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install xmllint
|
||||
run: sudo apt install -y libxml2-utils
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxml2-utils
|
||||
|
||||
- name: Verify input version
|
||||
env:
|
||||
NEW_VERSION: ${{ inputs.version_number }}
|
||||
- name: Get current version
|
||||
id: current-version
|
||||
run: |
|
||||
CURRENT_VERSION=$(xmllint --xpath '
|
||||
string(/manifest/@*[local-name()="versionName"
|
||||
string(/manifest/@*[local-name()="versionName"
|
||||
and namespace-uri()="http://schemas.android.com/apk/res/android"])
|
||||
' src/App/Platforms/Android/AndroidManifest.xml)
|
||||
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Verify input version
|
||||
if: ${{ inputs.version_number_override != '' }}
|
||||
env:
|
||||
CURRENT_VERSION: ${{ steps.current-version.outputs.version }}
|
||||
NEW_VERSION: ${{ inputs.version_number_override }}
|
||||
run: |
|
||||
# Error if version has not changed.
|
||||
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
||||
echo "Version has not changed."
|
||||
@@ -89,40 +110,93 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Bump Version - Android XML
|
||||
- name: Calculate next release version
|
||||
if: ${{ inputs.version_number_override == '' }}
|
||||
id: calculate-next-version
|
||||
uses: bitwarden/gh-actions/version-next@main
|
||||
with:
|
||||
version: ${{ steps.current-version.outputs.version }}
|
||||
|
||||
- name: Bump Version - Android XML - Version Override
|
||||
if: ${{ inputs.version_number_override != '' }}
|
||||
id: bump-version-override
|
||||
uses: bitwarden/gh-actions/version-bump@main
|
||||
with:
|
||||
version: ${{ inputs.version_number }}
|
||||
file_path: "src/App/Platforms/Android/AndroidManifest.xml"
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Bump Version - iOS.Autofill
|
||||
- name: Bump Version - Android XML - Automatic Calculation
|
||||
if: ${{ inputs.version_number_override == '' }}
|
||||
id: bump-version-automatic
|
||||
uses: bitwarden/gh-actions/version-bump@main
|
||||
with:
|
||||
file_path: "src/App/Platforms/Android/AndroidManifest.xml"
|
||||
version: ${{ steps.calculate-next-version.outputs.version }}
|
||||
|
||||
- name: Bump Version - iOS.Autofill - Version Override
|
||||
if: ${{ inputs.version_number_override != '' }}
|
||||
uses: bitwarden/gh-actions/version-bump@main
|
||||
with:
|
||||
version: ${{ inputs.version_number }}
|
||||
file_path: "src/iOS.Autofill/Info.plist"
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Bump Version - iOS.Extension
|
||||
- name: Bump Version - iOS.Autofill - Automatic Calculation
|
||||
if: ${{ inputs.version_number_override == '' }}
|
||||
uses: bitwarden/gh-actions/version-bump@main
|
||||
with:
|
||||
file_path: "src/iOS.Autofill/Info.plist"
|
||||
version: ${{ steps.calculate-next-version.outputs.version }}
|
||||
|
||||
- name: Bump Version - iOS.Extension - Version Override
|
||||
if: ${{ inputs.version_number_override != '' }}
|
||||
uses: bitwarden/gh-actions/version-bump@main
|
||||
with:
|
||||
version: ${{ inputs.version_number }}
|
||||
file_path: "src/iOS.Extension/Info.plist"
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Bump Version - iOS.ShareExtension
|
||||
- name: Bump Version - iOS.Extension - Automatic Calculation
|
||||
if: ${{ inputs.version_number_override == '' }}
|
||||
uses: bitwarden/gh-actions/version-bump@main
|
||||
with:
|
||||
file_path: "src/iOS.Extension/Info.plist"
|
||||
version: ${{ steps.calculate-next-version.outputs.version }}
|
||||
|
||||
- name: Bump Version - iOS.ShareExtension - Version Override
|
||||
if: ${{ inputs.version_number_override != '' }}
|
||||
uses: bitwarden/gh-actions/version-bump@main
|
||||
with:
|
||||
version: ${{ inputs.version_number }}
|
||||
file_path: "src/iOS.ShareExtension/Info.plist"
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Bump Version - iOS
|
||||
- name: Bump Version - iOS.ShareExtension - Automatic Calculation
|
||||
if: ${{ inputs.version_number_override == '' }}
|
||||
uses: bitwarden/gh-actions/version-bump@main
|
||||
with:
|
||||
version: ${{ inputs.version_number }}
|
||||
file_path: "src/App/Platforms/iOS/Info.plist"
|
||||
file_path: "src/iOS.ShareExtension/Info.plist"
|
||||
version: ${{ steps.calculate-next-version.outputs.version }}
|
||||
|
||||
- name: Setup git
|
||||
- name: Bump Version - iOS - Version Override
|
||||
if: ${{ inputs.version_number_override != '' }}
|
||||
uses: bitwarden/gh-actions/version-bump@main
|
||||
with:
|
||||
file_path: "src/App/Platforms/iOS/Info.plist"
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Bump Version - iOS - Automatic Calculation
|
||||
if: ${{ inputs.version_number_override == '' }}
|
||||
uses: bitwarden/gh-actions/version-bump@main
|
||||
with:
|
||||
file_path: "src/App/Platforms/iOS/Info.plist"
|
||||
version: ${{ steps.calculate-next-version.outputs.version }}
|
||||
|
||||
- name: Set Job output
|
||||
id: set-final-version-output
|
||||
run: |
|
||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||
git config --local user.name "bitwarden-devops-bot"
|
||||
if [[ "${{ steps.bump-version-override.outcome }}" == "success" ]]; then
|
||||
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ steps.bump-version-automatic.outcome }}" == "success" ]]; then
|
||||
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check if version changed
|
||||
id: version-changed
|
||||
@@ -136,7 +210,7 @@ jobs:
|
||||
|
||||
- name: Commit files
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
run: git commit -m "Bumped version to ${{ inputs.version_number }}" -a
|
||||
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
|
||||
|
||||
- name: Push changes
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
@@ -150,7 +224,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||
TITLE: "Bump version to ${{ inputs.version_number }}"
|
||||
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
|
||||
run: |
|
||||
PR_URL=$(gh pr create --title "$TITLE" \
|
||||
--base "main" \
|
||||
@@ -166,16 +240,18 @@ jobs:
|
||||
- [X] Other
|
||||
|
||||
## Objective
|
||||
Automated version bump to ${{ inputs.version_number }}")
|
||||
Automated version bump to ${{ steps.set-final-version-output.outputs.version }}")
|
||||
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Approve PR
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||
run: gh pr review $PR_NUMBER --approve
|
||||
|
||||
- name: Merge PR
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||
@@ -183,8 +259,8 @@ jobs:
|
||||
|
||||
cut_rc:
|
||||
name: Cut RC branch
|
||||
needs: bump_version
|
||||
if: ${{ inputs.cut_rc_branch == true }}
|
||||
needs: bump_version
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout Branch
|
||||
@@ -192,20 +268,28 @@ jobs:
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Install xmllint
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxml2-utils
|
||||
|
||||
- name: Verify version has been updated
|
||||
env:
|
||||
NEW_VERSION: ${{ inputs.version_number }}
|
||||
NEW_VERSION: ${{ needs.bump_version.outputs.version }}
|
||||
run: |
|
||||
# Wait for version to change.
|
||||
do
|
||||
while : ; do
|
||||
echo "Waiting for version to be updated..."
|
||||
git pull --force
|
||||
CURRENT_VERSION=$(xmllint --xpath '
|
||||
string(/manifest/@*[local-name()="versionName"
|
||||
string(/manifest/@*[local-name()="versionName"
|
||||
and namespace-uri()="http://schemas.android.com/apk/res/android"])
|
||||
' src/App/Platforms/Android/AndroidManifest.xml)
|
||||
|
||||
# If the versions don't match we continue the loop, otherwise we break out of the loop.
|
||||
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
|
||||
sleep 10
|
||||
done while [[ "$NEW_VERSION" != "$CURRENT_VERSION" ]]
|
||||
done
|
||||
|
||||
- name: Cut RC branch
|
||||
run: |
|
||||
|
||||
11
.github/workflows/workflow-linter.yml
vendored
11
.github/workflows/workflow-linter.yml
vendored
@@ -1,11 +0,0 @@
|
||||
---
|
||||
name: Workflow Linter
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/**
|
||||
|
||||
jobs:
|
||||
call-workflow:
|
||||
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@main
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -148,6 +148,7 @@ publish/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
!**/Xamarin.AndroidX.Credentials.1.0.0.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<MauiVersion>8.0.4-nightly.*</MauiVersion>
|
||||
<MauiVersion>8.0.7</MauiVersion>
|
||||
<ReleaseCodesignProvision>Automatic:AppStore</ReleaseCodesignProvision>
|
||||
<ReleaseCodesignKey>iPhone Distribution</ReleaseCodesignKey>
|
||||
<IncludeBitwardeniOSExtensions>True</IncludeBitwardeniOSExtensions>
|
||||
@@ -9,5 +9,8 @@
|
||||
|
||||
<!-- Uncomment this when Unit Testing-->
|
||||
<!-- <CustomConstants>UT</CustomConstants> -->
|
||||
|
||||
<!-- Uncomment this when building FDROID-->
|
||||
<!-- <CustomConstants>FDROID</CustomConstants> -->
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -2,9 +2,9 @@ project_id_env: _CROWDIN_PROJECT_ID
|
||||
api_token_env: CROWDIN_API_TOKEN
|
||||
preserve_hierarchy: true
|
||||
files:
|
||||
- source: /src/App/Resources/AppResources.resx
|
||||
dest: /src/App/Resources/%original_file_name%
|
||||
translation: /src/App/Resources/AppResources.%two_letters_code%.resx
|
||||
- source: /src/Core/Resources/Localization/AppResources.resx
|
||||
dest: /src/Core/Resources/Localization/%original_file_name%
|
||||
translation: /src/Core/Resources/Localization/AppResources.%two_letters_code%.resx
|
||||
update_option: update_as_unapproved
|
||||
languages_mapping:
|
||||
two_letters_code:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0"?>
|
||||
<doc>
|
||||
<assembly>
|
||||
<name>Xamarin.AndroidX.Credentials</name>
|
||||
</assembly>
|
||||
<members>
|
||||
</members>
|
||||
</doc>
|
||||
Binary file not shown.
@@ -2,5 +2,6 @@
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="MAUI Nightly builds" value="https://pkgs.dev.azure.com/xamarin/public/_packaging/maui-nightly/nuget/v3/index.json" />
|
||||
<add key="Local AndroidX Credentials" value="lib/android/Xamarin.AndroidX.Credentials" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
@@ -121,6 +121,7 @@
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
||||
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.18" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Activity.Ktx" Version="1.7.2.1" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Credentials" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' AND !$(DefineConstants.Contains(FDROID))">
|
||||
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet" Version="118.0.1.5" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?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="2024.2.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2024.3.3" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
@@ -43,6 +43,9 @@
|
||||
<!-- 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="android.support.customtabs.action.CustomTabsService" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="http" />
|
||||
|
||||
@@ -347,7 +347,7 @@ namespace Bit.Droid.Autofill
|
||||
// InlinePresentation requires nonNull pending intent (even though we only utilize one for the
|
||||
// "my vault" presentation) so we're including an empty one here
|
||||
pendingIntent = PendingIntent.GetService(context, 0, new Intent(),
|
||||
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent, true));
|
||||
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent, false));
|
||||
}
|
||||
var slice = CreateInlinePresentationSlice(
|
||||
inlinePresentationSpec,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Bit.Droid.Autofill
|
||||
{
|
||||
public class CredentialProviderConstants
|
||||
{
|
||||
public const string CredentialProviderCipherId = "credentialProviderCipherId";
|
||||
public const string CredentialDataIntentExtra = "CREDENTIAL_DATA";
|
||||
public const string CredentialIdIntentExtra = "credId";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Threading.Tasks;
|
||||
using Android.App;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
using AndroidX.Credentials.Provider;
|
||||
using AndroidX.Credentials.WebAuthn;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.App.Droid.Utilities;
|
||||
|
||||
namespace Bit.Droid.Autofill
|
||||
{
|
||||
[Activity(
|
||||
NoHistory = true,
|
||||
LaunchMode = LaunchMode.SingleTop)]
|
||||
public class CredentialProviderSelectionActivity : MauiAppCompatActivity
|
||||
{
|
||||
protected override void OnCreate(Bundle bundle)
|
||||
{
|
||||
Intent?.Validate();
|
||||
base.OnCreate(bundle);
|
||||
|
||||
var cipherId = Intent?.GetStringExtra(CredentialProviderConstants.CredentialProviderCipherId);
|
||||
if (string.IsNullOrEmpty(cipherId))
|
||||
{
|
||||
SetResult(Result.Canceled);
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
GetCipherAndPerformPasskeyAuthAsync(cipherId).FireAndForget();
|
||||
}
|
||||
|
||||
private async Task GetCipherAndPerformPasskeyAuthAsync(string cipherId)
|
||||
{
|
||||
// TODO this is a work in progress
|
||||
// https://developer.android.com/training/sign-in/credential-provider#passkeys-implement
|
||||
|
||||
var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(Intent);
|
||||
// var publicKeyRequest = getRequest?.CredentialOptions as PublicKeyCredentialRequestOptions;
|
||||
|
||||
var requestInfo = Intent.GetBundleExtra(CredentialProviderConstants.CredentialDataIntentExtra);
|
||||
var credIdEnc = requestInfo?.GetString(CredentialProviderConstants.CredentialIdIntentExtra);
|
||||
|
||||
var cipherService = ServiceContainer.Resolve<ICipherService>();
|
||||
var cipher = await cipherService.GetAsync(cipherId);
|
||||
var decCipher = await cipher.DecryptAsync();
|
||||
|
||||
var passkey = decCipher.Login.Fido2Credentials.Find(f => f.CredentialId == credIdEnc);
|
||||
|
||||
var credId = Convert.FromBase64String(credIdEnc);
|
||||
// var privateKey = Convert.FromBase64String(passkey.PrivateKey);
|
||||
// var uid = Convert.FromBase64String(passkey.uid);
|
||||
|
||||
var origin = getRequest?.CallingAppInfo.Origin;
|
||||
var packageName = getRequest?.CallingAppInfo.PackageName;
|
||||
|
||||
// --- continue WIP here (save TOTP copy as last step) ---
|
||||
|
||||
// Copy TOTP if needed
|
||||
var autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||
autofillHandler.Autofill(decCipher);
|
||||
}
|
||||
}
|
||||
}
|
||||
147
src/App/Platforms/Android/Autofill/CredentialProviderService.cs
Normal file
147
src/App/Platforms/Android/Autofill/CredentialProviderService.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using Android;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Graphics.Drawables;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using AndroidX.Credentials.Provider;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using AndroidX.Credentials.Exceptions;
|
||||
using AndroidX.Credentials.WebAuthn;
|
||||
using Bit.Core.Models.View;
|
||||
using Resource = Microsoft.Maui.Resource;
|
||||
|
||||
namespace Bit.Droid.Autofill
|
||||
{
|
||||
[Service(Permission = Manifest.Permission.BindCredentialProviderService, Label = "Bitwarden", Exported = true)]
|
||||
[IntentFilter(new string[] { "android.service.credentials.CredentialProviderService" })]
|
||||
[MetaData("android.credentials.provider", Resource = "@xml/provider")]
|
||||
[Register("com.x8bit.bitwarden.Autofill.CredentialProviderService")]
|
||||
public class CredentialProviderService : AndroidX.Credentials.Provider.CredentialProviderService
|
||||
{
|
||||
private const string GetPasskeyIntentAction = "PACKAGE_NAME.GET_PASSKEY";
|
||||
private const int UniqueRequestCode = 94556023;
|
||||
|
||||
private ICipherService _cipherService;
|
||||
private IUserVerificationService _userVerificationService;
|
||||
private IVaultTimeoutService _vaultTimeoutService;
|
||||
private LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
|
||||
public override async void OnBeginCreateCredentialRequest(BeginCreateCredentialRequest request,
|
||||
CancellationSignal cancellationSignal, IOutcomeReceiver callback) => throw new NotImplementedException();
|
||||
|
||||
public override async void OnBeginGetCredentialRequest(BeginGetCredentialRequest request,
|
||||
CancellationSignal cancellationSignal, IOutcomeReceiver callback)
|
||||
{
|
||||
try
|
||||
{
|
||||
_vaultTimeoutService ??= ServiceContainer.Resolve<IVaultTimeoutService>();
|
||||
|
||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||
var locked = await _vaultTimeoutService.IsLockedAsync();
|
||||
if (!locked)
|
||||
{
|
||||
var response = await ProcessGetCredentialsRequestAsync(request);
|
||||
callback.OnResult(response);
|
||||
}
|
||||
// TODO handle auth/unlock account flow
|
||||
}
|
||||
catch (GetCredentialException e)
|
||||
{
|
||||
_logger.Value.Exception(e);
|
||||
callback.OnError(e.ErrorMessage ?? "Error getting credentials");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Value.Exception(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<BeginGetCredentialResponse> ProcessGetCredentialsRequestAsync(
|
||||
BeginGetCredentialRequest request)
|
||||
{
|
||||
IList<CredentialEntry> credentialEntries = null;
|
||||
|
||||
foreach (var option in request.BeginGetCredentialOptions)
|
||||
{
|
||||
var credentialOption = option as BeginGetPublicKeyCredentialOption;
|
||||
if (credentialOption != null)
|
||||
{
|
||||
credentialEntries ??= new List<CredentialEntry>();
|
||||
((List<CredentialEntry>)credentialEntries).AddRange(
|
||||
await PopulatePasskeyDataAsync(request.CallingAppInfo, credentialOption));
|
||||
}
|
||||
}
|
||||
|
||||
if (credentialEntries == null)
|
||||
{
|
||||
return new BeginGetCredentialResponse();
|
||||
}
|
||||
|
||||
return new BeginGetCredentialResponse.Builder()
|
||||
.SetCredentialEntries(credentialEntries)
|
||||
.Build();
|
||||
}
|
||||
|
||||
private async Task<List<CredentialEntry>> PopulatePasskeyDataAsync(CallingAppInfo callingAppInfo,
|
||||
BeginGetPublicKeyCredentialOption option)
|
||||
{
|
||||
var packageName = callingAppInfo.PackageName;
|
||||
var origin = callingAppInfo.Origin;
|
||||
var signingInfo = callingAppInfo.SigningInfo;
|
||||
|
||||
var request = new PublicKeyCredentialRequestOptions(option.RequestJson);
|
||||
|
||||
var passkeyEntries = new List<CredentialEntry>();
|
||||
|
||||
_cipherService ??= ServiceContainer.Resolve<ICipherService>();
|
||||
var ciphers = await _cipherService.GetAllDecryptedForUrlAsync(origin);
|
||||
if (ciphers == null)
|
||||
{
|
||||
return passkeyEntries;
|
||||
}
|
||||
|
||||
var passkeyCiphers = ciphers.Where(cipher => cipher.HasFido2Credential).ToList();
|
||||
if (!passkeyCiphers.Any())
|
||||
{
|
||||
return passkeyEntries;
|
||||
}
|
||||
|
||||
foreach (var cipher in passkeyCiphers)
|
||||
{
|
||||
var passkeyEntry = GetPasskey(cipher, option);
|
||||
passkeyEntries.Add(passkeyEntry);
|
||||
}
|
||||
|
||||
return passkeyEntries;
|
||||
}
|
||||
|
||||
private PublicKeyCredentialEntry GetPasskey(CipherView cipher, BeginGetPublicKeyCredentialOption option)
|
||||
{
|
||||
var credDataBundle = new Bundle();
|
||||
credDataBundle.PutString(CredentialProviderConstants.CredentialIdIntentExtra,
|
||||
cipher.Login.MainFido2Credential.CredentialId);
|
||||
|
||||
var intent = new Intent(ApplicationContext, typeof(CredentialProviderSelectionActivity))
|
||||
.SetAction(GetPasskeyIntentAction).SetPackage(Constants.PACKAGE_NAME);
|
||||
intent.PutExtra(CredentialProviderConstants.CredentialDataIntentExtra, credDataBundle);
|
||||
intent.PutExtra(CredentialProviderConstants.CredentialProviderCipherId, cipher.Id);
|
||||
var pendingIntent = PendingIntent.GetActivity(ApplicationContext, UniqueRequestCode, intent,
|
||||
PendingIntentFlags.Mutable | PendingIntentFlags.UpdateCurrent);
|
||||
|
||||
return new PublicKeyCredentialEntry.Builder(
|
||||
ApplicationContext,
|
||||
cipher.Login.Username ?? "No username",
|
||||
pendingIntent,
|
||||
option)
|
||||
.SetDisplayName(cipher.Name)
|
||||
.SetIcon(Icon.CreateWithResource(ApplicationContext, Resource.Drawable.icon))
|
||||
.Build();
|
||||
}
|
||||
|
||||
public override void OnClearCredentialStateRequest(ProviderClearCredentialStateRequest request,
|
||||
CancellationSignal cancellationSignal, IOutcomeReceiver callback) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,12 @@ namespace Bit.Droid
|
||||
ServiceContainer.Resolve<IWatchDeviceService>(),
|
||||
ServiceContainer.Resolve<IConditionedAwaiterManager>());
|
||||
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
||||
|
||||
var userPinService = new UserPinService(
|
||||
ServiceContainer.Resolve<IStateService>(),
|
||||
ServiceContainer.Resolve<ICryptoService>(),
|
||||
ServiceContainer.Resolve<IVaultTimeoutService>());
|
||||
ServiceContainer.Register<IUserPinService>(userPinService);
|
||||
}
|
||||
#if !FDROID
|
||||
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
|
||||
@@ -160,7 +166,6 @@ namespace Bit.Droid
|
||||
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
|
||||
var cryptoService = new CryptoService(stateService, cryptoFunctionService, logger);
|
||||
var biometricService = new BiometricService(stateService, cryptoService);
|
||||
var userPinService = new UserPinService(stateService, cryptoService);
|
||||
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService, stateService);
|
||||
|
||||
ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage);
|
||||
@@ -184,7 +189,6 @@ namespace Bit.Droid
|
||||
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
|
||||
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
|
||||
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
|
||||
ServiceContainer.Register<IUserPinService>(userPinService);
|
||||
|
||||
// Push
|
||||
#if FDROID
|
||||
|
||||
6
src/App/Platforms/Android/Resources/xml/provider.xml
Normal file
6
src/App/Platforms/Android/Resources/xml/provider.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<credential-provider xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<capabilities>
|
||||
<capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
|
||||
</capabilities>
|
||||
</credential-provider>
|
||||
@@ -79,24 +79,29 @@ namespace Bit.Droid.Services
|
||||
}
|
||||
|
||||
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 intent = context.PackageManager?.GetLaunchIntentForPackage(context.PackageName ?? string.Empty);
|
||||
|
||||
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);
|
||||
if(intent != null && context.PackageManager != null && !string.IsNullOrEmpty(context.PackageName))
|
||||
{
|
||||
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, Bit.Core.Constants.AndroidNotificationChannelId)
|
||||
.SetContentIntent(pendingIntent)
|
||||
.SetContentTitle(title)
|
||||
var deleteIntent = new Intent(context, typeof(NotificationDismissReceiver));
|
||||
deleteIntent.PutExtra(Bit.Core.Constants.NotificationData, JsonConvert.SerializeObject(data));
|
||||
var deletePendingIntent = PendingIntent.GetBroadcast(context, 20220802, deleteIntent, pendingIntentFlags);
|
||||
|
||||
builder.SetContentIntent(pendingIntent)
|
||||
.SetDeleteIntent(deletePendingIntent);
|
||||
}
|
||||
|
||||
builder.SetContentTitle(title)
|
||||
.SetContentText(message)
|
||||
.SetSmallIcon(Bit.Core.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);
|
||||
|
||||
@@ -37,6 +37,23 @@ namespace Bit.Droid.Services
|
||||
_eventService = eventService;
|
||||
}
|
||||
|
||||
public bool CredentialProviderServiceEnabled()
|
||||
{
|
||||
if (Build.VERSION.SdkInt < BuildVersionCodes.UpsideDownCake)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
// TODO - find a way to programmatically check if the credential provider service is enabled
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool AutofillServiceEnabled()
|
||||
{
|
||||
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
||||
@@ -163,7 +180,14 @@ namespace Bit.Droid.Services
|
||||
return Accessibility.AccessibilityHelpers.OverlayPermitted();
|
||||
}
|
||||
|
||||
|
||||
public void DisableCredentialProviderService()
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO - find a way to programmatically disable the provider service, or take the user to the settings page where they can do it
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public void DisableAutofillService()
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ using Android.Text.Method;
|
||||
using Android.Views;
|
||||
using Android.Views.InputMethods;
|
||||
using Android.Widget;
|
||||
using AndroidX.Credentials;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
@@ -72,17 +73,28 @@ namespace Bit.Droid.Services
|
||||
|
||||
public bool LaunchApp(string appName)
|
||||
{
|
||||
if ((int)Build.VERSION.SdkInt < 33)
|
||||
try
|
||||
{
|
||||
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 = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
appName = appName.Replace("androidapp://", string.Empty);
|
||||
var launchIntentSender = activity?.PackageManager?.GetLaunchIntentSenderForPackage(appName);
|
||||
launchIntentSender?.SendIntent(activity, Result.Ok, null, null, null);
|
||||
return launchIntentSender != null;
|
||||
}
|
||||
catch (IntentSender.SendIntentException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (Android.Util.AndroidException)
|
||||
{
|
||||
// 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 = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
appName = appName.Replace("androidapp://", string.Empty);
|
||||
var launchIntentSender = activity?.PackageManager?.GetLaunchIntentSenderForPackage(appName);
|
||||
launchIntentSender?.SendIntent(activity, Result.Ok, null, null, null);
|
||||
return launchIntentSender != null;
|
||||
}
|
||||
|
||||
public async Task ShowLoadingAsync(string text)
|
||||
@@ -490,6 +502,27 @@ namespace Bit.Droid.Services
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenCredentialProviderSettings()
|
||||
{
|
||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
try
|
||||
{
|
||||
var pendingIntent = CredentialManager.Create(activity).CreateSettingsPendingIntent();
|
||||
pendingIntent.Send();
|
||||
}
|
||||
catch (ActivityNotFoundException)
|
||||
{
|
||||
var alertBuilder = new AlertDialog.Builder(activity);
|
||||
alertBuilder.SetMessage(AppResources.BitwardenCredentialProviderGoToSettings);
|
||||
alertBuilder.SetCancelable(true);
|
||||
alertBuilder.SetPositiveButton(AppResources.Ok, (sender, args) =>
|
||||
{
|
||||
(sender as AlertDialog)?.Cancel();
|
||||
});
|
||||
alertBuilder.Create().Show();
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenAccessibilitySettings()
|
||||
{
|
||||
try
|
||||
@@ -548,6 +581,8 @@ namespace Bit.Droid.Services
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SupportsCredentialProviderService() => Build.VERSION.SdkInt >= BuildVersionCodes.UpsideDownCake;
|
||||
|
||||
public bool SupportsAutofillServices() => Build.VERSION.SdkInt >= BuildVersionCodes.O;
|
||||
|
||||
public bool SupportsInlineAutofill() => Build.VERSION.SdkInt >= BuildVersionCodes.R;
|
||||
|
||||
@@ -8,6 +8,7 @@ using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Droid.Accessibility;
|
||||
using Java.Lang;
|
||||
using Bit.App.Droid.Utilities;
|
||||
|
||||
namespace Bit.Droid.Tile
|
||||
{
|
||||
@@ -76,7 +77,7 @@ namespace Bit.Droid.Tile
|
||||
var intent = new Intent(this, typeof(AccessibilityActivity));
|
||||
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
|
||||
intent.PutExtra("autofillTileClicked", true);
|
||||
StartActivityAndCollapse(intent);
|
||||
this.StartActivityAndCollapseWithIntent(intent, isMutable: true);
|
||||
}
|
||||
|
||||
private void ShowConfigErrorDialog()
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Service.QuickSettings;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using Bit.App.Droid.Utilities;
|
||||
using Java.Lang;
|
||||
|
||||
namespace Bit.Droid.Tile
|
||||
@@ -62,7 +55,7 @@ namespace Bit.Droid.Tile
|
||||
var intent = new Intent(this, typeof(MainActivity));
|
||||
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
|
||||
intent.PutExtra("generatorTile", true);
|
||||
StartActivityAndCollapse(intent);
|
||||
this.StartActivityAndCollapseWithIntent(intent, isMutable: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Service.QuickSettings;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using Bit.App.Droid.Utilities;
|
||||
using Java.Lang;
|
||||
|
||||
namespace Bit.Droid.Tile
|
||||
@@ -63,7 +56,7 @@ namespace Bit.Droid.Tile
|
||||
var intent = new Intent(this, typeof(MainActivity));
|
||||
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
|
||||
intent.PutExtra("myVaultTile", true);
|
||||
StartActivityAndCollapse(intent);
|
||||
this.StartActivityAndCollapseWithIntent(intent, isMutable: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Provider;
|
||||
using Android.Service.QuickSettings;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
namespace Bit.App.Droid.Utilities
|
||||
@@ -64,5 +65,26 @@ namespace Bit.App.Droid.Utilities
|
||||
|
||||
return pendingIntentFlags;
|
||||
}
|
||||
|
||||
public static void StartActivityAndCollapseWithIntent(this TileService service, Intent intent, bool isMutable)
|
||||
{
|
||||
//For Android 14+ We need to use PendingIntent instead of Intent directly. Older versions still need to use Intent.
|
||||
if (Build.VERSION.SdkInt < BuildVersionCodes.UpsideDownCake)
|
||||
{
|
||||
service.StartActivityAndCollapse(intent);
|
||||
return;
|
||||
}
|
||||
var pendingIntent = PendingIntent.GetActivity(
|
||||
service.ApplicationContext,
|
||||
0,
|
||||
intent,
|
||||
AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, isMutable)
|
||||
);
|
||||
if (pendingIntent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
service.StartActivityAndCollapse(pendingIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace Bit.iOS
|
||||
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
||||
if (needsAutofillReplacement.GetValueOrDefault())
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
else if (message.Command == "showAppExtension")
|
||||
@@ -102,7 +102,7 @@ namespace Bit.iOS
|
||||
var success = value as bool?;
|
||||
if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12)
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,22 +114,21 @@ namespace Bit.iOS
|
||||
return;
|
||||
}
|
||||
|
||||
if (await ASHelpers.IdentitiesCanIncremental())
|
||||
if (await ASHelpers.IdentitiesSupportIncrementalAsync())
|
||||
{
|
||||
var cipherId = message.Data as string;
|
||||
if (message.Command == "addedCipher" && !string.IsNullOrWhiteSpace(cipherId))
|
||||
{
|
||||
var identity = await ASHelpers.GetCipherIdentityAsync(cipherId);
|
||||
var identity = await ASHelpers.GetCipherPasswordIdentityAsync(cipherId);
|
||||
if (identity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await ASCredentialIdentityStore.SharedStore?.SaveCredentialIdentitiesAsync(
|
||||
new ASPasswordCredentialIdentity[] { identity });
|
||||
await ASCredentialIdentityStoreExtensions.SaveCredentialIdentitiesAsync(identity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher")
|
||||
{
|
||||
@@ -138,28 +137,27 @@ namespace Bit.iOS
|
||||
return;
|
||||
}
|
||||
|
||||
if (await ASHelpers.IdentitiesCanIncremental())
|
||||
if (await ASHelpers.IdentitiesSupportIncrementalAsync())
|
||||
{
|
||||
var identity = ASHelpers.ToCredentialIdentity(
|
||||
var identity = ASHelpers.ToPasswordCredentialIdentity(
|
||||
message.Data as Bit.Core.Models.View.CipherView);
|
||||
if (identity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveCredentialIdentitiesAsync(
|
||||
new ASPasswordCredentialIdentity[] { identity });
|
||||
await ASCredentialIdentityStoreExtensions.RemoveCredentialIdentitiesAsync(identity);
|
||||
return;
|
||||
}
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
else if (message.Command == "logout" && UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||
}
|
||||
else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher")
|
||||
&& UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND)
|
||||
{
|
||||
@@ -168,12 +166,12 @@ namespace Bit.iOS
|
||||
{
|
||||
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.8bit.bitwarden</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2024.2.1</string>
|
||||
<string>2024.3.3</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CFBundleIconName</key>
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IAutofillHandler
|
||||
{
|
||||
bool CredentialProviderServiceEnabled();
|
||||
bool AutofillServicesEnabled();
|
||||
bool SupportsAutofillService();
|
||||
void Autofill(CipherView cipher);
|
||||
@@ -11,6 +12,7 @@ namespace Bit.Core.Abstractions
|
||||
bool AutofillAccessibilityServiceRunning();
|
||||
bool AutofillAccessibilityOverlayPermitted();
|
||||
bool AutofillServiceEnabled();
|
||||
void DisableCredentialProviderService();
|
||||
void DisableAutofillService();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.View;
|
||||
@@ -37,5 +34,7 @@ namespace Bit.Core.Abstractions
|
||||
Task<byte[]> DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId);
|
||||
Task SoftDeleteWithServerAsync(string id);
|
||||
Task RestoreWithServerAsync(string id);
|
||||
Task<string> CreateNewLoginForPasskeyAsync(Fido2ConfirmNewCredentialParams newPasskeyParams);
|
||||
Task CopyTotpCodeIfNeededAsync(CipherView cipher);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public enum AwaiterPrecondition
|
||||
{
|
||||
EnvironmentUrlsInited
|
||||
EnvironmentUrlsInited,
|
||||
AndroidWindowCreated,
|
||||
AutofillIOSExtensionViewDidAppear
|
||||
}
|
||||
|
||||
public interface IConditionedAwaiterManager
|
||||
@@ -13,5 +12,6 @@ namespace Bit.Core.Abstractions
|
||||
Task GetAwaiterForPrecondition(AwaiterPrecondition awaiterPrecondition);
|
||||
void SetAsCompleted(AwaiterPrecondition awaiterPrecondition);
|
||||
void SetException(AwaiterPrecondition awaiterPrecondition, Exception ex);
|
||||
void Recreate(AwaiterPrecondition awaiterPrecondition);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace Bit.App.Abstractions
|
||||
bool SupportsNfc();
|
||||
bool SupportsCamera();
|
||||
bool SupportsFido2();
|
||||
bool SupportsCredentialProviderService();
|
||||
bool SupportsAutofillServices();
|
||||
bool SupportsInlineAutofill();
|
||||
bool SupportsDrawOver();
|
||||
@@ -36,6 +37,7 @@ namespace Bit.App.Abstractions
|
||||
void RateApp();
|
||||
void OpenAccessibilitySettings();
|
||||
void OpenAccessibilityOverlayPermissionSettings();
|
||||
void OpenCredentialProviderSettings();
|
||||
void OpenAutofillSettings();
|
||||
long GetActiveTime();
|
||||
void CloseMainApp();
|
||||
|
||||
12
src/Core/Abstractions/IFido2AuthenticatorService.cs
Normal file
12
src/Core/Abstractions/IFido2AuthenticatorService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IFido2AuthenticatorService
|
||||
{
|
||||
Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams, IFido2MakeCredentialUserInterface userInterface);
|
||||
Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface);
|
||||
// TODO: Should this return a List? Or maybe IEnumerable?
|
||||
Task<Fido2AuthenticatorDiscoverableCredentialMetadata[]> SilentCredentialDiscoveryAsync(string rpId);
|
||||
}
|
||||
}
|
||||
35
src/Core/Abstractions/IFido2ClientService.cs
Normal file
35
src/Core/Abstractions/IFido2ClientService.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
/// <summary>
|
||||
/// This class represents an abstraction of the WebAuthn Client as described by W3C:
|
||||
/// https://www.w3.org/TR/webauthn-3/#webauthn-client
|
||||
///
|
||||
/// The WebAuthn Client is an intermediary entity typically implemented in the user agent
|
||||
/// (in whole, or in part). Conceptually, it underlies the Web Authentication API and embodies
|
||||
/// the implementation of the Web Authentication API's operations.
|
||||
///
|
||||
/// It is responsible for both marshalling the inputs for the underlying authenticator operations,
|
||||
/// and for returning the results of the latter operations to the Web Authentication API's callers.
|
||||
/// </summary>
|
||||
public interface IFido2ClientService
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows WebAuthn Relying Party scripts to request the creation of a new public key credential source.
|
||||
/// For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-createCredential
|
||||
/// </summary>
|
||||
/// <param name="createCredentialParams">The parameters for the credential creation operation</param>
|
||||
/// <returns>The new credential</returns>
|
||||
Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams);
|
||||
|
||||
/// <summary>
|
||||
/// Allows WebAuthn Relying Party scripts to discover and use an existing public key credential, with the user’s consent.
|
||||
/// Relying Party script can optionally specify some criteria to indicate what credential sources are acceptable to it.
|
||||
/// For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-getAssertion
|
||||
/// </summary>
|
||||
/// <param name="assertCredentialParams">The parameters for the credential assertion operation</param>
|
||||
/// <returns>The asserted credential</returns>
|
||||
Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams);
|
||||
}
|
||||
}
|
||||
20
src/Core/Abstractions/IFido2GetAssertionUserInterface.cs
Normal file
20
src/Core/Abstractions/IFido2GetAssertionUserInterface.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public struct Fido2GetAssertionUserInterfaceCredential
|
||||
{
|
||||
public string CipherId { get; set; }
|
||||
public Fido2UserVerificationPreference UserVerificationPreference { get; set; }
|
||||
}
|
||||
|
||||
public interface IFido2GetAssertionUserInterface : IFido2UserInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// Ask the user to pick a credential from a list of existing credentials.
|
||||
/// </summary>
|
||||
/// <param name="credentials">The credentials that the user can pick from, and if the user must be verified before completing the operation</param>
|
||||
/// <returns>The ID of the cipher that contains the credentials the user picked, and if the user was verified before completing the operation</returns>
|
||||
Task<(string CipherId, bool UserVerified)> PickCredentialAsync(Fido2GetAssertionUserInterfaceCredential[] credentials);
|
||||
}
|
||||
}
|
||||
44
src/Core/Abstractions/IFido2MakeCredentialUserInterface.cs
Normal file
44
src/Core/Abstractions/IFido2MakeCredentialUserInterface.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public struct Fido2ConfirmNewCredentialParams
|
||||
{
|
||||
///<summary>
|
||||
/// The name of the credential.
|
||||
///</summary>
|
||||
public string CredentialName { get; set; }
|
||||
|
||||
///<summary>
|
||||
/// The name of the user.
|
||||
///</summary>
|
||||
public string UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The preference to whether or not the user must be verified before completing the operation.
|
||||
/// </summary>
|
||||
public Fido2UserVerificationPreference UserVerificationPreference { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The relying party identifier
|
||||
/// </summary>
|
||||
public string RpId { get; set; }
|
||||
}
|
||||
|
||||
public interface IFido2MakeCredentialUserInterface : IFido2UserInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// Inform the user that the operation was cancelled because their vault contains excluded credentials.
|
||||
/// </summary>
|
||||
/// <param name="existingCipherIds">The IDs of the excluded credentials.</param>
|
||||
/// <returns>When user has confirmed the message</returns>
|
||||
Task InformExcludedCredentialAsync(string[] existingCipherIds);
|
||||
|
||||
/// <summary>
|
||||
/// Ask the user to confirm the creation of a new credential.
|
||||
/// </summary>
|
||||
/// <param name="confirmNewCredentialParams">The parameters to use when asking the user to confirm the creation of a new credential.</param>
|
||||
/// <returns>The ID of the cipher where the new credential should be saved, and if the user was verified before completing the operation</returns>
|
||||
Task<(string CipherId, bool UserVerified)> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams);
|
||||
}
|
||||
}
|
||||
14
src/Core/Abstractions/IFido2MediatorService.cs
Normal file
14
src/Core/Abstractions/IFido2MediatorService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IFido2MediatorService
|
||||
{
|
||||
Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams);
|
||||
Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams);
|
||||
|
||||
Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams, IFido2MakeCredentialUserInterface userInterface);
|
||||
Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface);
|
||||
Task<Fido2AuthenticatorDiscoverableCredentialMetadata[]> SilentCredentialDiscoveryAsync(string rpId);
|
||||
}
|
||||
}
|
||||
17
src/Core/Abstractions/IFido2UserInterface.cs
Normal file
17
src/Core/Abstractions/IFido2UserInterface.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IFido2UserInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the vault has been unlocked during this transaction
|
||||
/// </summary>
|
||||
bool HasVaultBeenUnlockedInThisTransaction { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Make sure that the vault is unlocked.
|
||||
/// This should open a window and ask the user to login or unlock the vault if necessary.
|
||||
/// </summary>
|
||||
/// <returns>When vault has been unlocked.</returns>
|
||||
Task EnsureUnlockedVaultAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
@@ -10,5 +9,7 @@ namespace Bit.App.Abstractions
|
||||
Task<bool> PromptAndCheckPasswordIfNeededAsync(CipherRepromptType repromptType = CipherRepromptType.Password);
|
||||
|
||||
Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
|
||||
|
||||
Task<bool> ShouldByPassMasterPasswordRepromptAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
@@ -29,7 +26,7 @@ namespace Bit.Core.Abstractions
|
||||
bool SupportsDuo();
|
||||
Task<bool> SupportsBiometricAsync();
|
||||
Task<bool> IsBiometricIntegrityValidAsync(string bioIntegritySrcKey = null);
|
||||
Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null, bool logOutOnTooManyAttempts = false);
|
||||
Task<bool?> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null, bool logOutOnTooManyAttempts = false, bool allowAlternativeAuthentication = false);
|
||||
long GetActiveTime();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +186,7 @@ namespace Bit.Core.Abstractions
|
||||
Task<BwRegion?> GetActiveUserRegionAsync();
|
||||
Task<BwRegion?> GetPreAuthRegionAsync();
|
||||
Task SetPreAuthRegionAsync(BwRegion value);
|
||||
Task ReloadStateAsync();
|
||||
[Obsolete("Use GetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")]
|
||||
Task<string> GetPinProtectedAsync(string userId = null);
|
||||
[Obsolete("Use SetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")]
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IUserPinService
|
||||
{
|
||||
Task<bool> IsPinLockEnabledAsync();
|
||||
Task SetupPinAsync(string pin, bool requireMasterPasswordOnRestart);
|
||||
Task<bool> VerifyPinAsync(string inputPin);
|
||||
Task<bool> VerifyPinAsync(string inputPin, string email, KdfConfig kdfConfig, PinLockType pinLockType);
|
||||
}
|
||||
}
|
||||
|
||||
28
src/Core/Abstractions/IUserVerificationMediatorService.cs
Normal file
28
src/Core/Abstractions/IUserVerificationMediatorService.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IUserVerificationMediatorService
|
||||
{
|
||||
Task<CancellableResult<bool>> VerifyUserForFido2Async(Fido2UserVerificationOptions options);
|
||||
Task<bool> CanPerformUserVerificationPreferredAsync(Fido2UserVerificationOptions options);
|
||||
Task<bool> ShouldPerformMasterPasswordRepromptAsync(Fido2UserVerificationOptions options);
|
||||
Task<bool> ShouldEnforceFido2RequiredUserVerificationAsync(Fido2UserVerificationOptions options);
|
||||
Task<CancellableResult<UVResult>> PerformOSUnlockAsync();
|
||||
Task<CancellableResult<UVResult>> VerifyPinCodeAsync();
|
||||
Task<CancellableResult<UVResult>> VerifyMasterPasswordAsync(bool isMasterPasswordReprompt);
|
||||
|
||||
public struct UVResult
|
||||
{
|
||||
public UVResult(bool canPerform, bool isVerified)
|
||||
{
|
||||
CanPerform = canPerform;
|
||||
IsVerified = isVerified;
|
||||
}
|
||||
|
||||
public bool CanPerform { get; set; }
|
||||
public bool IsVerified { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IUserVerificationService
|
||||
{
|
||||
Task<bool> VerifyUser(string secret, VerificationType verificationType);
|
||||
Task<bool> VerifyMasterPasswordAsync(string masterPassword);
|
||||
Task<bool> HasMasterPasswordAsync(bool checkMasterKeyHash = false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Pages;
|
||||
using Bit.Core.Services;
|
||||
@@ -46,7 +47,6 @@ namespace Bit.App
|
||||
// This queue keeps those actions so that when the app has resumed they can still be executed.
|
||||
// Links: https://github.com/dotnet/maui/issues/11501 and https://bitwarden.atlassian.net/wiki/spaces/NMME/pages/664862722/MainPage+Assignments+not+working+on+Android+on+Background+or+App+resume
|
||||
private readonly Queue<Action> _onResumeActions = new Queue<Action>();
|
||||
private bool _hasNavigatedToAutofillWindow;
|
||||
|
||||
#if ANDROID
|
||||
|
||||
@@ -120,41 +120,8 @@ namespace Bit.App
|
||||
return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally)
|
||||
}
|
||||
|
||||
//"Internal" Autofill and Uri/Otp/CreateSend. This is where we create the autofill specific Window
|
||||
if (Options != null && (Options.FromAutofillFramework || Options.Uri != null || Options.OtpData != null || Options.CreateSend != null))
|
||||
{
|
||||
_isResumed = true; //Specifically for the Autofill scenario we need to manually set the _isResumed here
|
||||
_hasNavigatedToAutofillWindow = true;
|
||||
return new AutoFillWindow(new NavigationPage(new AndroidNavigationRedirectPage()));
|
||||
}
|
||||
|
||||
var homePage = new HomePage(Options);
|
||||
// WORKAROUND: If the user autofills with Accessibility Services enabled and goes back to the application then there is currently an issue
|
||||
// where this method is called again
|
||||
// thus it goes through here and the user goes to HomePage as we see here.
|
||||
// So to solve this, the next flag check has been added which then turns on a flag on the home page
|
||||
// that will trigger a navigation on the accounts manager when it loads; workarounding this behavior and navigating the user
|
||||
// to the proper page depending on its state.
|
||||
// WARNING: this doens't navigate the user to where they were but it acts as if the user had changed their account.
|
||||
if(_hasNavigatedToAutofillWindow)
|
||||
{
|
||||
homePage.PerformNavigationOnAccountChangedOnLoad = true;
|
||||
// this is needed because when coming back from AutofillWindow OnResume won't be called and we need this flag
|
||||
// so that void Navigate(NavigationTarget navTarget, INavigationParams navParams) doesn't enqueue the navigation
|
||||
// and it performs it directly.
|
||||
_isResumed = true;
|
||||
_hasNavigatedToAutofillWindow = false;
|
||||
}
|
||||
|
||||
//If we have an existing MainAppWindow we can use that one
|
||||
var mainAppWindow = Windows.OfType<MainAppWindow>().FirstOrDefault();
|
||||
if (mainAppWindow != null)
|
||||
{
|
||||
mainAppWindow.PendingPage = new NavigationPage(homePage);
|
||||
}
|
||||
|
||||
//Create new main window
|
||||
return new MainAppWindow(new NavigationPage(homePage));
|
||||
_isResumed = true;
|
||||
return new ResumeWindow(new NavigationPage(new AndroidNavigationRedirectPage(Options)));
|
||||
}
|
||||
#else
|
||||
//iOS doesn't use the CreateWindow override used in Android so we just set the Application.Current.MainPage directly
|
||||
@@ -171,7 +138,7 @@ namespace Bit.App
|
||||
Application.Current.MainPage = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
public App() : this(null)
|
||||
@@ -201,132 +168,153 @@ namespace Bit.App
|
||||
|
||||
_accountsManager.Init(() => Options, this);
|
||||
|
||||
Bootstrap();
|
||||
_broadcasterService.Subscribe(nameof(App), async (message) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (message.Command == "showDialog")
|
||||
{
|
||||
var details = message.Data as DialogDetails;
|
||||
var confirmed = true;
|
||||
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
|
||||
AppResources.Ok : details.ConfirmText;
|
||||
await MainThread.InvokeOnMainThreadAsync(async () =>
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(details.CancelText))
|
||||
{
|
||||
confirmed = await MainPage.DisplayAlert(details.Title, details.Text, confirmText,
|
||||
details.CancelText);
|
||||
}
|
||||
else
|
||||
{
|
||||
await MainPage.DisplayAlert(details.Title, details.Text, confirmText);
|
||||
}
|
||||
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
|
||||
});
|
||||
}
|
||||
#if IOS
|
||||
else if (message.Command == AppHelpers.RESUMED_MESSAGE_COMMAND)
|
||||
{
|
||||
ResumedAsync().FireAndForget();
|
||||
}
|
||||
else if (message.Command == "slept")
|
||||
{
|
||||
await SleptAsync();
|
||||
}
|
||||
#endif
|
||||
else if (message.Command == "migrated")
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
await _accountsManager.NavigateOnAccountChangeAsync();
|
||||
}
|
||||
else if (message.Command == POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE ||
|
||||
message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE ||
|
||||
message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE ||
|
||||
message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE ||
|
||||
message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
|
||||
{
|
||||
if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
|
||||
{
|
||||
Options.OtpData = new OtpData((string)message.Data);
|
||||
}
|
||||
_broadcasterService.Subscribe(nameof(App), BroadcastServiceMessageCallbackAsync);
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(async () =>
|
||||
Bootstrap();
|
||||
}
|
||||
|
||||
private async void BroadcastServiceMessageCallbackAsync(Message message)
|
||||
{
|
||||
try
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(message);
|
||||
if (message.Command == "showDialog")
|
||||
{
|
||||
var details = message.Data as DialogDetails;
|
||||
ArgumentNullException.ThrowIfNull(details);
|
||||
ArgumentNullException.ThrowIfNull(MainPage);
|
||||
|
||||
var confirmed = true;
|
||||
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
|
||||
AppResources.Ok : details.ConfirmText;
|
||||
await MainThread.InvokeOnMainThreadAsync(ShowDialogAction);
|
||||
async Task ShowDialogAction()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(details.CancelText))
|
||||
{
|
||||
if (MainPage is TabsPage tabsPage)
|
||||
confirmed = await MainPage.DisplayAlert(details.Title, details.Text, confirmText,
|
||||
details.CancelText);
|
||||
}
|
||||
else
|
||||
{
|
||||
await MainPage.DisplayAlert(details.Title, details.Text, confirmText);
|
||||
}
|
||||
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
|
||||
}
|
||||
}
|
||||
#if IOS
|
||||
else if (message.Command == AppHelpers.RESUMED_MESSAGE_COMMAND)
|
||||
{
|
||||
ResumedAsync().FireAndForget();
|
||||
}
|
||||
else if (message.Command == "slept")
|
||||
{
|
||||
await SleptAsync();
|
||||
}
|
||||
#endif
|
||||
else if (message.Command == "migrated")
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
await _accountsManager.NavigateOnAccountChangeAsync();
|
||||
}
|
||||
else if (message.Command == POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE ||
|
||||
message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE ||
|
||||
message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE ||
|
||||
message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE ||
|
||||
message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
|
||||
{
|
||||
if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
|
||||
{
|
||||
Options.OtpData = new OtpData((string)message.Data);
|
||||
}
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(ExecuteNavigationAction);
|
||||
async Task ExecuteNavigationAction()
|
||||
{
|
||||
if (MainPage is TabsPage tabsPage)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tabsPage.Navigation);
|
||||
ArgumentNullException.ThrowIfNull(tabsPage.Navigation.ModalStack);
|
||||
while (tabsPage.Navigation.ModalStack.Count > 0)
|
||||
{
|
||||
while (tabsPage.Navigation.ModalStack.Count > 0)
|
||||
{
|
||||
await tabsPage.Navigation.PopModalAsync(false);
|
||||
}
|
||||
if (message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE)
|
||||
{
|
||||
MainPage = new NavigationPage(new CipherSelectionPage(Options));
|
||||
}
|
||||
else if (message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE)
|
||||
{
|
||||
Options.MyVaultTile = false;
|
||||
tabsPage.ResetToVaultPage();
|
||||
}
|
||||
else if (message.Command == POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE)
|
||||
{
|
||||
Options.GeneratorTile = false;
|
||||
tabsPage.ResetToGeneratorPage();
|
||||
}
|
||||
else if (message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE)
|
||||
{
|
||||
tabsPage.ResetToSendPage();
|
||||
}
|
||||
else if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
|
||||
{
|
||||
tabsPage.ResetToVaultPage();
|
||||
await tabsPage.Navigation.PushModalAsync(new NavigationPage(new CipherSelectionPage(Options)));
|
||||
}
|
||||
await tabsPage.Navigation.PopModalAsync(false);
|
||||
}
|
||||
if (message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE)
|
||||
{
|
||||
MainPage = new NavigationPage(new CipherSelectionPage(Options));
|
||||
}
|
||||
else if (message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE)
|
||||
{
|
||||
Options.MyVaultTile = false;
|
||||
tabsPage.ResetToVaultPage();
|
||||
}
|
||||
else if (message.Command == POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE)
|
||||
{
|
||||
Options.GeneratorTile = false;
|
||||
tabsPage.ResetToGeneratorPage();
|
||||
}
|
||||
else if (message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE)
|
||||
{
|
||||
tabsPage.ResetToSendPage();
|
||||
}
|
||||
else if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
|
||||
{
|
||||
tabsPage.ResetToVaultPage();
|
||||
ArgumentNullException.ThrowIfNull(tabsPage.Navigation);
|
||||
await tabsPage.Navigation.PushModalAsync(new NavigationPage(new CipherSelectionPage(Options)));
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (message.Command == "convertAccountToKeyConnector")
|
||||
{
|
||||
await MainThread.InvokeOnMainThreadAsync(async () =>
|
||||
{
|
||||
await MainPage.Navigation.PushModalAsync(
|
||||
new NavigationPage(new RemoveMasterPasswordPage()));
|
||||
});
|
||||
}
|
||||
else if (message.Command == Constants.ForceUpdatePassword)
|
||||
{
|
||||
await MainThread.InvokeOnMainThreadAsync(async () =>
|
||||
{
|
||||
await MainPage.Navigation.PushModalAsync(
|
||||
new NavigationPage(new UpdateTempPasswordPage()));
|
||||
});
|
||||
}
|
||||
else if (message.Command == Constants.ForceSetPassword)
|
||||
{
|
||||
await MainThread.InvokeOnMainThreadAsync(() => MainPage.Navigation.PushModalAsync(
|
||||
new NavigationPage(new SetPasswordPage(orgIdentifier: (string)message.Data))));
|
||||
}
|
||||
else if (message.Command == "syncCompleted")
|
||||
{
|
||||
await _configService.GetAsync(true);
|
||||
}
|
||||
else if (message.Command == Constants.PasswordlessLoginRequestKey
|
||||
|| message.Command == "unlocked"
|
||||
|| message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
|
||||
{
|
||||
lock (_processingLoginRequestLock)
|
||||
{
|
||||
// lock doesn't allow for async execution
|
||||
CheckPasswordlessLoginRequestsAsync().Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
else if (message.Command == "convertAccountToKeyConnector")
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
ArgumentNullException.ThrowIfNull(MainPage);
|
||||
await MainThread.InvokeOnMainThreadAsync(NavigateToRemoveMasterPasswordPageAction);
|
||||
async Task NavigateToRemoveMasterPasswordPageAction()
|
||||
{
|
||||
await MainPage.Navigation.PushModalAsync(
|
||||
new NavigationPage(new RemoveMasterPasswordPage()));
|
||||
}
|
||||
}
|
||||
});
|
||||
else if (message.Command == Constants.ForceUpdatePassword)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(MainPage);
|
||||
await MainThread.InvokeOnMainThreadAsync(NavigateToUpdateTempPasswordPageAction);
|
||||
async Task NavigateToUpdateTempPasswordPageAction()
|
||||
{
|
||||
await MainPage.Navigation.PushModalAsync(
|
||||
new NavigationPage(new UpdateTempPasswordPage()));
|
||||
}
|
||||
}
|
||||
else if (message.Command == Constants.ForceSetPassword)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(MainPage);
|
||||
await MainThread.InvokeOnMainThreadAsync(NavigateToSetPasswordPageAction);
|
||||
void NavigateToSetPasswordPageAction()
|
||||
{
|
||||
MainPage.Navigation.PushModalAsync(
|
||||
new NavigationPage(new SetPasswordPage(orgIdentifier: (string)message.Data)));
|
||||
}
|
||||
}
|
||||
else if (message.Command == "syncCompleted")
|
||||
{
|
||||
await _configService.GetAsync(true);
|
||||
}
|
||||
else if (message.Command == Constants.PasswordlessLoginRequestKey
|
||||
|| message.Command == "unlocked"
|
||||
|| message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
|
||||
{
|
||||
lock (_processingLoginRequestLock)
|
||||
{
|
||||
// lock doesn't allow for async execution
|
||||
CheckPasswordlessLoginRequestsAsync().Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckPasswordlessLoginRequestsAsync()
|
||||
@@ -341,7 +329,6 @@ namespace Bit.App
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var notification = await _stateService.GetPasswordlessLoginNotificationAsync();
|
||||
if (notification == null)
|
||||
{
|
||||
|
||||
@@ -47,6 +47,7 @@ namespace Bit.Core
|
||||
public const string ConfigsKey = "configsKey";
|
||||
public const string DisplayEuEnvironmentFlag = "display-eu-environment";
|
||||
public const string RegionEnvironment = "regionEnvironment";
|
||||
public const string DuoCallback = "bitwarden://duo-callback";
|
||||
|
||||
/// <summary>
|
||||
/// This key is used to store the value of "ShouldConnectToWatch" of the last user that had logged in
|
||||
|
||||
@@ -53,12 +53,13 @@ namespace Bit.App.Controls
|
||||
if (BindingContext is CipherItemViewModel cipherItemVM)
|
||||
{
|
||||
cipherItemVM.IconImageSuccesfullyLoaded = true;
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
Icon.IsVisible = cipherItemVM.ShowIconImage;
|
||||
IconPlaceholder.IsVisible = !cipherItemVM.ShowIconImage;
|
||||
});
|
||||
}
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
Icon.IsVisible = true;
|
||||
IconPlaceholder.IsVisible = false;
|
||||
});
|
||||
}
|
||||
|
||||
public void Icon_Error(object sender, FFImageLoading.Maui.CachedImageEvents.ErrorEventArgs e)
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
||||
<PackageReference Include="LiteDB" Version="5.0.17" />
|
||||
<PackageReference Include="PCLCrypto" Version="2.1.40-alpha" />
|
||||
<PackageReference Include="System.Formats.Cbor" Version="8.0.0" />
|
||||
<PackageReference Include="zxcvbn-core" Version="7.0.92" />
|
||||
<PackageReference Include="MessagePack.MSBuild.Tasks" Version="2.5.124">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@@ -52,6 +53,7 @@
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
||||
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.18" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Activity.Ktx" Version="1.7.2.1" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Credentials" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' AND !$(DefineConstants.Contains(FDROID))">
|
||||
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet" Version="118.0.1.5" />
|
||||
@@ -75,8 +77,11 @@
|
||||
<Folder Include="Utilities\Automation\" />
|
||||
<Folder Include="Utilities\Prompts\" />
|
||||
<Folder Include="Resources\Localization\" />
|
||||
<Folder Include="Utilities\Fido2\" />
|
||||
<Folder Include="Controls\Picker\" />
|
||||
<Folder Include="Controls\Avatar\" />
|
||||
<Folder Include="Services\UserVerification\" />
|
||||
<Folder Include="Utilities\WebAuthenticatorMAUI\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<MauiImage Include="Resources\Images\dotnet_bot.svg">
|
||||
@@ -105,7 +110,10 @@
|
||||
</MauiXaml>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Utilities\Fido2\" />
|
||||
<None Remove="Controls\Picker\" />
|
||||
<None Remove="Controls\Avatar\" />
|
||||
<None Remove="Services\UserVerification\" />
|
||||
<None Remove="Utilities\WebAuthenticatorMAUI\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.Core.Models.Api
|
||||
{
|
||||
@@ -21,6 +20,7 @@ namespace Bit.Core.Models.Api
|
||||
RpName = fido2Key.RpName?.EncryptedString;
|
||||
UserHandle = fido2Key.UserHandle?.EncryptedString;
|
||||
UserName = fido2Key.UserName?.EncryptedString;
|
||||
UserDisplayName = fido2Key.UserDisplayName?.EncryptedString;
|
||||
Counter = fido2Key.Counter?.EncryptedString;
|
||||
CreationDate = fido2Key.CreationDate;
|
||||
}
|
||||
@@ -35,6 +35,7 @@ namespace Bit.Core.Models.Api
|
||||
public string RpName { get; set; }
|
||||
public string UserHandle { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string UserDisplayName { get; set; }
|
||||
public string Counter { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace Bit.Core.Models.Data
|
||||
RpName = apiData.RpName;
|
||||
UserHandle = apiData.UserHandle;
|
||||
UserName = apiData.UserName;
|
||||
UserDisplayName = apiData.UserDisplayName;
|
||||
Counter = apiData.Counter;
|
||||
CreationDate = apiData.CreationDate;
|
||||
}
|
||||
@@ -33,6 +34,7 @@ namespace Bit.Core.Models.Data
|
||||
public string RpName { get; set; }
|
||||
public string UserHandle { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string UserDisplayName { get; set; }
|
||||
public string Counter { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.View;
|
||||
|
||||
namespace Bit.Core.Models.Domain
|
||||
@@ -21,6 +17,7 @@ namespace Bit.Core.Models.Domain
|
||||
nameof(RpName),
|
||||
nameof(UserHandle),
|
||||
nameof(UserName),
|
||||
nameof(UserDisplayName),
|
||||
nameof(Counter)
|
||||
};
|
||||
|
||||
@@ -48,6 +45,7 @@ namespace Bit.Core.Models.Domain
|
||||
public EncString RpName { get; set; }
|
||||
public EncString UserHandle { get; set; }
|
||||
public EncString UserName { get; set; }
|
||||
public EncString UserDisplayName { get; set; }
|
||||
public EncString Counter { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Domain
|
||||
{
|
||||
@@ -9,7 +8,7 @@ namespace Bit.Core.Models.Domain
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
throw new Exception("Must provide key.");
|
||||
throw new ArgumentKeyNullException(nameof(key));
|
||||
}
|
||||
|
||||
if (encType == null)
|
||||
@@ -24,7 +23,7 @@ namespace Bit.Core.Models.Domain
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Unable to determine encType.");
|
||||
throw new InvalidKeyOperationException("Unable to determine encType.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +47,7 @@ namespace Bit.Core.Models.Domain
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Unsupported encType/key length.");
|
||||
throw new InvalidKeyOperationException("Unsupported encType/key length.");
|
||||
}
|
||||
|
||||
if (Key != null)
|
||||
@@ -72,6 +71,32 @@ namespace Bit.Core.Models.Domain
|
||||
public string KeyB64 { get; set; }
|
||||
public string EncKeyB64 { get; set; }
|
||||
public string MacKeyB64 { get; set; }
|
||||
|
||||
public class ArgumentKeyNullException : ArgumentNullException
|
||||
{
|
||||
public ArgumentKeyNullException(string paramName) : base(paramName)
|
||||
{
|
||||
}
|
||||
|
||||
public ArgumentKeyNullException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public ArgumentKeyNullException(string paramName, string message) : base(paramName, message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class InvalidKeyOperationException : InvalidOperationException
|
||||
{
|
||||
public InvalidKeyOperationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidKeyOperationException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class UserKey : SymmetricCryptoKey
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.Core.Models.View
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Models.View
|
||||
{
|
||||
@@ -26,13 +26,42 @@ namespace Bit.Core.Models.View
|
||||
public string RpName { get; set; }
|
||||
public string UserHandle { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string UserDisplayName { get; set; }
|
||||
public string Counter { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public int CounterValue {
|
||||
get => int.TryParse(Counter, out var counter) ? counter : 0;
|
||||
set => Counter = value.ToString();
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public byte[] UserHandleValue {
|
||||
get => UserHandle == null ? null : CoreHelpers.Base64UrlDecode(UserHandle);
|
||||
set => UserHandle = value == null ? null : CoreHelpers.Base64UrlEncode(value);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public byte[] KeyBytes {
|
||||
get => KeyValue == null ? null : CoreHelpers.Base64UrlDecode(KeyValue);
|
||||
set => KeyValue = value == null ? null : CoreHelpers.Base64UrlEncode(value);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool DiscoverableValue {
|
||||
get => bool.TryParse(Discoverable, out var discoverable) && discoverable;
|
||||
set => Discoverable = value.ToString().ToLower();
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public override string SubTitle => UserName;
|
||||
|
||||
public override List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions => new List<KeyValuePair<string, LinkedIdType>>();
|
||||
public bool IsDiscoverable => !string.IsNullOrWhiteSpace(Discoverable);
|
||||
|
||||
[JsonIgnore]
|
||||
public bool CanLaunch => !string.IsNullOrEmpty(RpId);
|
||||
[JsonIgnore]
|
||||
public string LaunchUri => $"https://{RpId}";
|
||||
|
||||
public bool IsUniqueAgainst(Fido2CredentialView fido2View) => fido2View?.RpId != RpId || fido2View?.UserName != UserName;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Models.View
|
||||
{
|
||||
@@ -40,4 +39,15 @@ namespace Bit.Core.Models.View
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class LoginViewExtensions
|
||||
{
|
||||
public static string GetMainFido2CredentialUsername(this LoginView loginView)
|
||||
{
|
||||
return loginView.MainFido2Credential.UserName
|
||||
.FallbackOnNullOrWhiteSpace(loginView.MainFido2Credential.UserDisplayName)
|
||||
.FallbackOnNullOrWhiteSpace(loginView.Username)
|
||||
.FallbackOnNullOrWhiteSpace(AppResources.UnknownAccount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
<ScrollView>
|
||||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="MAUI APP"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n SelfHostedEnvironment, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Platform;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -26,7 +27,7 @@ namespace Bit.App.Pages
|
||||
_apiEntry.ReturnCommand = new Command(() => _identityEntry.Focus());
|
||||
_identityEntry.ReturnType = ReturnType.Next;
|
||||
_identityEntry.ReturnCommand = new Command(() => _iconsEntry.Focus());
|
||||
_vm.SubmitSuccessAction = () => MainThread.BeginInvokeOnMainThread(async () => await SubmitSuccessAsync());
|
||||
_vm.SubmitSuccessTask = () => MainThread.InvokeOnMainThreadAsync(SubmitSuccessAsync);
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
@@ -37,6 +38,12 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);
|
||||
await Navigation.PopModalAsync();
|
||||
#if ANDROID
|
||||
if (Platform.CurrentActivity.CurrentFocus != null)
|
||||
{
|
||||
Platform.CurrentActivity.HideKeyboard(Platform.CurrentActivity.CurrentFocus);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace Bit.App.Pages
|
||||
public string WebVaultUrl { get; set; }
|
||||
public string IconsUrl { get; set; }
|
||||
public string NotificationsUrls { get; set; }
|
||||
public Action SubmitSuccessAction { get; set; }
|
||||
public Func<Task> SubmitSuccessTask { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
public async Task SubmitAsync()
|
||||
@@ -73,7 +73,10 @@ namespace Bit.App.Pages
|
||||
IconsUrl = resUrls.Icons;
|
||||
NotificationsUrls = resUrls.Notifications;
|
||||
|
||||
SubmitSuccessAction?.Invoke();
|
||||
if (SubmitSuccessTask != null)
|
||||
{
|
||||
await SubmitSuccessTask();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ValidateUrls()
|
||||
|
||||
@@ -12,12 +12,14 @@ namespace Bit.App.Pages
|
||||
private readonly HomeViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
private IBroadcasterService _broadcasterService;
|
||||
private IConditionedAwaiterManager _conditionedAwaiterManager;
|
||||
|
||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||
|
||||
public HomePage(AppOptions appOptions = null)
|
||||
{
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
|
||||
_conditionedAwaiterManager = ServiceContainer.Resolve<IConditionedAwaiterManager>();
|
||||
_appOptions = appOptions;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as HomeViewModel;
|
||||
@@ -56,6 +58,8 @@ namespace Bit.App.Pages
|
||||
PerformNavigationOnAccountChangedOnLoad = false;
|
||||
accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
|
||||
}
|
||||
|
||||
_conditionedAwaiterManager.SetAsCompleted(AwaiterPrecondition.AndroidWindowCreated);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -515,7 +515,7 @@ namespace Bit.App.Pages
|
||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||
PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
|
||||
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)),
|
||||
!PinEnabled && !HasMasterPassword);
|
||||
!PinEnabled && !HasMasterPassword) ?? false;
|
||||
|
||||
await _stateService.SetBiometricLockedAsync(!success);
|
||||
if (success)
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
FontSize="Small"
|
||||
FontAttributes="Bold"/>
|
||||
<controls:MonoLabel
|
||||
FormattedText="{Binding LoginRequest.FingerprintPhrase}"
|
||||
Text="{Binding LoginRequest.FingerprintPhrase}"
|
||||
FontSize="Medium"
|
||||
TextColor="{DynamicResource FingerprintPhrase}"
|
||||
Margin="0,0,0,27"
|
||||
@@ -85,7 +85,6 @@
|
||||
<Button
|
||||
Text="{u:I18n DenyLogIn}"
|
||||
Command="{Binding RejectRequestCommand}"
|
||||
StyleClass="btn-secundary"
|
||||
AutomationId="DenyLoginButton" />
|
||||
|
||||
</StackLayout>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
FontSize="Small"
|
||||
FontAttributes="Bold" />
|
||||
<controls:MonoLabel
|
||||
FormattedText="{Binding FingerprintPhrase}"
|
||||
Text="{Binding FingerprintPhrase}"
|
||||
FontSize="Small"
|
||||
TextColor="{DynamicResource FingerprintPhrase}"
|
||||
AutomationId="FingerprintPhraseValue" />
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace Bit.App.Pages
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as LoginSsoPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.FromIosExtension = _appOptions?.IosExtension ?? false;
|
||||
_vm.StartTwoFactorAction = () => MainThread.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||
_vm.StartSetPasswordAction = () =>
|
||||
MainThread.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
|
||||
|
||||
@@ -15,6 +15,16 @@ using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Authentication;
|
||||
using Microsoft.Maui.Networking;
|
||||
using NetworkAccess = Microsoft.Maui.Networking.NetworkAccess;
|
||||
using Org.BouncyCastle.Asn1.Ocsp;
|
||||
|
||||
#if IOS
|
||||
using AuthenticationServices;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
using WebAuthenticator = Bit.Core.Utilities.MAUI.WebAuthenticator;
|
||||
using WebAuthenticatorResult = Bit.Core.Utilities.MAUI.WebAuthenticatorResult;
|
||||
using WebAuthenticatorOptions = Bit.Core.Utilities.MAUI.WebAuthenticatorOptions;
|
||||
#endif
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -64,6 +74,8 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _orgIdentifier, value);
|
||||
}
|
||||
|
||||
public bool FromIosExtension { get; set; }
|
||||
|
||||
public ICommand LogInCommand { get; }
|
||||
public Action StartTwoFactorAction { get; set; }
|
||||
public Action StartSetPasswordAction { get; set; }
|
||||
@@ -153,6 +165,9 @@ namespace Bit.App.Pages
|
||||
CallbackUrl = new Uri(REDIRECT_URI),
|
||||
Url = new Uri(url),
|
||||
PrefersEphemeralWebBrowserSession = _useEphemeralWebBrowserSession,
|
||||
#if IOS
|
||||
ShouldUseSharedApplicationKeyWindow = FromIosExtension
|
||||
#endif
|
||||
});
|
||||
|
||||
var code = GetResultCode(authResult, state);
|
||||
|
||||
@@ -132,14 +132,26 @@
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="0" Padding="0" IsVisible="{Binding DuoMethod, Mode=OneWay}"
|
||||
VerticalOptions="StartAndExpand">
|
||||
<StackLayout
|
||||
Spacing="0"
|
||||
Padding="0"
|
||||
IsVisible="{Binding DuoMethod, Mode=OneWay}"
|
||||
VerticalOptions="FillAndExpand">
|
||||
<Label
|
||||
StyleClass="box"
|
||||
Text="{Binding DuoFramelessLabel}"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
Margin="10,21"
|
||||
IsVisible="{Binding IsDuoFrameless}"/>
|
||||
<controls:HybridWebView
|
||||
x:Name="_duoWebView"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HeightRequest="{Binding DuoWebViewHeight, Mode=OneWay}" />
|
||||
<StackLayout StyleClass="box" VerticalOptions="End">
|
||||
HeightRequest="{Binding DuoWebViewHeight, Mode=OneWay}"
|
||||
IsVisible="{Binding IsDuoFrameless, Converter={StaticResource inverseBool}}"/>
|
||||
<StackLayout
|
||||
StyleClass="box"
|
||||
VerticalOptions="End">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n RememberMe}"
|
||||
@@ -151,6 +163,12 @@
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<Button Text="{u:I18n LaunchDuo}"
|
||||
Margin="10,21"
|
||||
StyleClass="btn-primary"
|
||||
Command="{Binding AuthenticateWithDuoFramelessCommand}"
|
||||
AutomationId="DuoFramelessButton"
|
||||
IsVisible="{Binding IsDuoFrameless}"/>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
Spacing="0"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@@ -34,6 +35,7 @@ namespace Bit.App.Pages
|
||||
private string _webVaultUrl = "https://vault.bitwarden.com";
|
||||
private bool _enableContinue = false;
|
||||
private bool _showContinue = true;
|
||||
private bool _isDuoFrameless = false;
|
||||
private double _duoWebViewHeight;
|
||||
|
||||
public TwoFactorPageViewModel()
|
||||
@@ -56,6 +58,7 @@ namespace Bit.App.Pages
|
||||
PageTitle = AppResources.TwoStepLogin;
|
||||
SubmitCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(async () => await SubmitAsync()), allowsMultipleExecutions: false);
|
||||
MoreCommand = CreateDefaultAsyncRelayCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||
AuthenticateWithDuoFramelessCommand = CreateDefaultAsyncRelayCommand(DuoFramelessAuthenticateAsync, allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public string TotpInstruction
|
||||
@@ -103,6 +106,16 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _enableContinue, value);
|
||||
}
|
||||
|
||||
public bool IsDuoFrameless
|
||||
{
|
||||
get => _isDuoFrameless;
|
||||
set => SetProperty(ref _isDuoFrameless, value, additionalPropertyNames: new string[] { nameof(DuoFramelessLabel) });
|
||||
}
|
||||
|
||||
public string DuoFramelessLabel => SelectedProviderType == TwoFactorProviderType.OrganizationDuo ?
|
||||
$"{AppResources.DuoTwoStepLoginIsRequiredForYourAccount} {AppResources.FollowTheStepsFromDuoToFinishLoggingIn}" :
|
||||
AppResources.FollowTheStepsFromDuoToFinishLoggingIn;
|
||||
|
||||
#if IOS
|
||||
public string YubikeyInstruction => AppResources.YubiKeyInstructionIos;
|
||||
#else
|
||||
@@ -125,6 +138,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
public ICommand SubmitCommand { get; }
|
||||
public ICommand MoreCommand { get; }
|
||||
public ICommand AuthenticateWithDuoFramelessCommand { get; }
|
||||
public Action TwoFactorAuthSuccessAction { get; set; }
|
||||
public Action LockAction { get; set; }
|
||||
public Action StartDeviceApprovalOptionsAction { get; set; }
|
||||
@@ -179,15 +193,29 @@ namespace Bit.App.Pages
|
||||
break;
|
||||
case TwoFactorProviderType.Duo:
|
||||
case TwoFactorProviderType.OrganizationDuo:
|
||||
SetDuoWebViewHeight();
|
||||
var host = WebUtility.UrlEncode(providerData["Host"] as string);
|
||||
var req = WebUtility.UrlEncode(providerData["Signature"] as string);
|
||||
page.DuoWebView.Uri = $"{_webVaultUrl}/duo-connector.html?host={host}&request={req}";
|
||||
page.DuoWebView.RegisterAction(sig =>
|
||||
IsDuoFrameless = providerData.ContainsKey("AuthUrl");
|
||||
if (!IsDuoFrameless)
|
||||
{
|
||||
Token = sig;
|
||||
SubmitCommand.Execute(null);
|
||||
});
|
||||
SetDuoWebViewHeight();
|
||||
var host = WebUtility.UrlEncode(providerData["Host"] as string);
|
||||
var req = WebUtility.UrlEncode(providerData["Signature"] as string);
|
||||
page.DuoWebView.Uri = $"{_webVaultUrl}/duo-connector.html?host={host}&request={req}";
|
||||
page.DuoWebView.RegisterAction(sig =>
|
||||
{
|
||||
Token = sig;
|
||||
MainThread.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await SubmitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleException(ex);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
break;
|
||||
case TwoFactorProviderType.Email:
|
||||
TotpInstruction = string.Format(AppResources.EnterVerificationCodeEmail,
|
||||
@@ -211,6 +239,77 @@ namespace Bit.App.Pages
|
||||
ShowContinue = !(SelectedProviderType == null || DuoMethod || Fido2Method);
|
||||
}
|
||||
|
||||
private async Task DuoFramelessAuthenticateAsync()
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
|
||||
|
||||
if (!_authService.TwoFactorProvidersData.TryGetValue(SelectedProviderType.Value, out var providerData) ||
|
||||
!providerData.TryGetValue("AuthUrl", out var urlObject))
|
||||
{
|
||||
throw new InvalidOperationException("Duo authentication error: Could not get ProviderData or AuthUrl");
|
||||
}
|
||||
|
||||
var url = urlObject as string;
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
throw new ArgumentNullException("Duo authentication error: Could not get valid auth url");
|
||||
}
|
||||
|
||||
WebAuthenticatorResult authResult;
|
||||
try
|
||||
{
|
||||
authResult = await WebAuthenticator.AuthenticateAsync(new WebAuthenticatorOptions
|
||||
{
|
||||
Url = new Uri(url),
|
||||
CallbackUrl = new Uri(Constants.DuoCallback)
|
||||
});
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// user canceled
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (authResult == null || authResult.Properties == null)
|
||||
{
|
||||
throw new InvalidOperationException("Duo authentication error: Could not get result from authentication");
|
||||
}
|
||||
|
||||
if (authResult.Properties.TryGetValue("error", out var resultError))
|
||||
{
|
||||
_logger.Error(resultError);
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
string code = null;
|
||||
if (authResult.Properties.TryGetValue("code", out var resultCodeData))
|
||||
{
|
||||
code = Uri.UnescapeDataString(resultCodeData);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
{
|
||||
throw new ArgumentException("Duo authentication error: response code is null or empty/whitespace");
|
||||
}
|
||||
|
||||
string state = null;
|
||||
if (authResult.Properties.TryGetValue("state", out var resultStateData))
|
||||
{
|
||||
state = Uri.UnescapeDataString(resultStateData);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(state))
|
||||
{
|
||||
throw new ArgumentException("Duo authentication error: response state is null or empty/whitespace");
|
||||
}
|
||||
|
||||
Token = $"{code}|{state}";
|
||||
await SubmitAsync(true);
|
||||
}
|
||||
|
||||
public void SetDuoWebViewHeight()
|
||||
{
|
||||
var screenHeight = DeviceDisplay.MainDisplayInfo.Height / DeviceDisplay.MainDisplayInfo.Density;
|
||||
|
||||
@@ -1,20 +1,40 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Pages;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Pages;
|
||||
|
||||
public partial class AndroidNavigationRedirectPage : ContentPage
|
||||
{
|
||||
private readonly IAccountsManager _accountsManager;
|
||||
|
||||
public AndroidNavigationRedirectPage()
|
||||
private AppOptions _options;
|
||||
public AndroidNavigationRedirectPage(AppOptions options)
|
||||
{
|
||||
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
||||
_options = options ?? new AppOptions();
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void AndroidNavigationRedirectPage_OnLoaded(object sender, EventArgs e)
|
||||
{
|
||||
_accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
|
||||
if (ServiceContainer.TryResolve<IAccountsManager>(out var accountsManager))
|
||||
{
|
||||
accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
|
||||
}
|
||||
else
|
||||
{
|
||||
Bit.App.App.MainPage = new NavigationPage(new HomePage(_options)); //Fallback scenario to load HomePage just in case something goes wrong when resolving IAccountsManager
|
||||
}
|
||||
|
||||
if (ServiceContainer.TryResolve<IConditionedAwaiterManager>(out var conditionedAwaiterManager))
|
||||
{
|
||||
conditionedAwaiterManager?.SetAsCompleted(AwaiterPrecondition.AndroidWindowCreated);
|
||||
}
|
||||
else
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(new InvalidOperationException("ConditionedAwaiterManager can't be resolved on Android Navigation redirection"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +266,8 @@
|
||||
AutomationId="SendShowHideOptionsButton" />
|
||||
<controls:IconButton
|
||||
x:Name="_btnOptionsUp"
|
||||
InputTransparent="True"
|
||||
MinimumWidthRequest="25"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.ChevronUp}}"
|
||||
StyleClass="box-row-button"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
@@ -274,6 +276,8 @@
|
||||
AutomationId="SendOptionsDisplayed" />
|
||||
<controls:IconButton
|
||||
x:Name="_btnOptionsDown"
|
||||
InputTransparent="True"
|
||||
MinimumWidthRequest="25"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.AngleDown}}"
|
||||
StyleClass="box-row-button"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
x:DataType="pages:SendGroupingsPageListItem">
|
||||
<controls:ExtendedStackLayout Orientation="Horizontal"
|
||||
StyleClass="list-row, list-row-platform"
|
||||
Spacing="6"
|
||||
AutomationId="{Binding AutomationId}">
|
||||
<controls:IconLabel Text="{Binding Icon, Mode=OneWay}"
|
||||
HorizontalOptions="Start"
|
||||
|
||||
@@ -68,7 +68,8 @@ namespace Bit.App.Pages
|
||||
{
|
||||
get
|
||||
{
|
||||
var appInfo = string.Format("{0}: {1} ({2})",
|
||||
// TODO: REMOVE WHEN MERGED INTO MAIN BRANCH
|
||||
var appInfo = string.Format("MAUI {0}: {1} ({2})",
|
||||
AppResources.Version,
|
||||
_platformUtilsService.GetApplicationVersion(),
|
||||
_deviceActionService.GetBuildNumber());
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
x:Class="Bit.App.Pages.AutofillPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
Title="{u:I18n PasswordAutofill}">
|
||||
Title="{u:I18n SetUpAutofill}">
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
@@ -15,26 +15,22 @@
|
||||
<StackLayout Spacing="5"
|
||||
Padding="20, 20, 20, 30"
|
||||
VerticalOptions="FillAndExpand">
|
||||
<Label Text="{u:I18n ExtensionInstantAccess}"
|
||||
<Label Text="{u:I18n GetInstantAccessToYourPasswordsAndPasskeys}"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap"
|
||||
StyleClass="text-lg"
|
||||
Margin="0, 0, 0, 15" />
|
||||
<Label Text="{u:I18n AutofillTurnOn}"
|
||||
<Label Text="{u:I18n SetUpAutoFillDescriptionLong}"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap"
|
||||
Margin="0, 0, 0, 15" />
|
||||
<Label Text="{u:I18n AutofillTurnOn1}"
|
||||
<Label Text="{u:I18n FirstDotGoToYourDeviceSettingsPasswordsPasswordOptions}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n AutofillTurnOn2}"
|
||||
<Label Text="{u:I18n SecondDotTurnOnAutoFill}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n AutofillTurnOn3}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n AutofillTurnOn4}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n AutofillTurnOn5}"
|
||||
<Label Text="{u:I18n ThirdDotSelectBitwardenToUseForPasswordsAndPasskeys}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Image Source="autofill-kb.png"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
|
||||
@@ -19,6 +19,15 @@
|
||||
Text="{u:I18n Autofill}"
|
||||
StyleClass="settings-header" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n CredentialProviderService}"
|
||||
Subtitle="{u:I18n CredentialProviderServiceExplanationLong}"
|
||||
IsVisible="{Binding SupportsCredentialProviderService}"
|
||||
IsToggled="{Binding UseCredentialProviderService}"
|
||||
AutomationId="CredentialProviderServiceSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n AutofillServices}"
|
||||
Subtitle="{u:I18n AutofillServicesExplanationLong}"
|
||||
|
||||
@@ -6,12 +6,27 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AutofillSettingsPageViewModel
|
||||
{
|
||||
private bool _useCredentialProviderService;
|
||||
private bool _useAutofillServices;
|
||||
private bool _useInlineAutofill;
|
||||
private bool _useAccessibility;
|
||||
private bool _useDrawOver;
|
||||
private bool _askToAddLogin;
|
||||
|
||||
public bool SupportsCredentialProviderService => DeviceInfo.Platform == DevicePlatform.Android && _deviceActionService.SupportsCredentialProviderService();
|
||||
|
||||
public bool UseCredentialProviderService
|
||||
{
|
||||
get => _useCredentialProviderService;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _useCredentialProviderService, value))
|
||||
{
|
||||
((ICommand)ToggleUseCredentialProviderServiceCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool SupportsAndroidAutofillServices => DeviceInfo.Platform == DevicePlatform.Android && _deviceActionService.SupportsAutofillServices();
|
||||
|
||||
public bool UseAutofillServices
|
||||
@@ -84,6 +99,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public AsyncRelayCommand ToggleUseCredentialProviderServiceCommand { get; private set; }
|
||||
public AsyncRelayCommand ToggleUseAutofillServicesCommand { get; private set; }
|
||||
public AsyncRelayCommand ToggleUseInlineAutofillCommand { get; private set; }
|
||||
public AsyncRelayCommand ToggleUseAccessibilityCommand { get; private set; }
|
||||
@@ -93,6 +109,7 @@ namespace Bit.App.Pages
|
||||
|
||||
private void InitAndroidCommands()
|
||||
{
|
||||
ToggleUseCredentialProviderServiceCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseCredentialProviderService()), () => _inited, allowsMultipleExecutions: false);
|
||||
ToggleUseAutofillServicesCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseAutofillServices()), () => _inited, allowsMultipleExecutions: false);
|
||||
ToggleUseInlineAutofillCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseInlineAutofillEnabledAsync()), () => _inited, allowsMultipleExecutions: false);
|
||||
ToggleUseAccessibilityCommand = CreateDefaultAsyncRelayCommand(ToggleUseAccessibilityAsync, () => _inited, allowsMultipleExecutions: false);
|
||||
@@ -115,6 +132,9 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task UpdateAndroidAutofillSettingsAsync()
|
||||
{
|
||||
// TODO - uncomment once _autofillHandler.CredentialProviderServiceEnabled() returns a real value
|
||||
// _useCredentialProviderService =
|
||||
// SupportsCredentialProviderService && _autofillHandler.CredentialProviderServiceEnabled();
|
||||
_useAutofillServices =
|
||||
_autofillHandler.SupportsAutofillService() && _autofillHandler.AutofillServiceEnabled();
|
||||
_useAccessibility = _autofillHandler.AutofillAccessibilityServiceRunning();
|
||||
@@ -123,6 +143,7 @@ namespace Bit.App.Pages
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
TriggerPropertyChanged(nameof(UseCredentialProviderService));
|
||||
TriggerPropertyChanged(nameof(UseAutofillServices));
|
||||
TriggerPropertyChanged(nameof(UseAccessibility));
|
||||
TriggerPropertyChanged(nameof(UseDrawOver));
|
||||
@@ -130,6 +151,18 @@ namespace Bit.App.Pages
|
||||
});
|
||||
}
|
||||
|
||||
private void ToggleUseCredentialProviderService()
|
||||
{
|
||||
if (UseCredentialProviderService)
|
||||
{
|
||||
_deviceActionService.OpenCredentialProviderSettings();
|
||||
}
|
||||
else
|
||||
{
|
||||
_autofillHandler.DisableCredentialProviderService();
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleUseAutofillServices()
|
||||
{
|
||||
if (UseAutofillServices)
|
||||
|
||||
@@ -37,10 +37,9 @@
|
||||
<Label
|
||||
Text="{u:I18n FingerprintPhrase}"
|
||||
FontSize="Small"
|
||||
Padding="0, 10, 0 ,0"
|
||||
FontAttributes="Bold"/>
|
||||
<controls:MonoLabel
|
||||
FormattedText="{Binding FingerprintPhrase}"
|
||||
Text="{Binding FingerprintPhrase}"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
FontSize="Small"
|
||||
@@ -70,64 +69,70 @@
|
||||
Grid.ColumnSpan="2"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<StackLayout
|
||||
x:Key="mainLayout"
|
||||
x:Name="_mainLayout"
|
||||
Padding="0, 10">
|
||||
<RefreshView
|
||||
IsRefreshing="{Binding IsRefreshing}"
|
||||
Command="{Binding RefreshCommand}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
BackgroundColor="{DynamicResource BackgroundColor}">
|
||||
<StackLayout>
|
||||
<Image
|
||||
x:Name="_emptyPlaceholder"
|
||||
Source="empty_login_requests"
|
||||
HorizontalOptions="Center"
|
||||
WidthRequest="160"
|
||||
HeightRequest="160"
|
||||
Margin="0,70,0,0"
|
||||
IsVisible="{Binding HasLoginRequests, Converter={StaticResource inverseBool}}"
|
||||
SemanticProperties.Description="{u:I18n NoPendingRequests}" />
|
||||
<controls:CustomLabel
|
||||
StyleClass="box-label-regular"
|
||||
Text="{u:I18n NoPendingRequests}"
|
||||
IsVisible="{Binding HasLoginRequests, Converter={StaticResource inverseBool}}"
|
||||
FontAttributes="{OnPlatform iOS=Bold}"
|
||||
FontWeight="500"
|
||||
HorizontalTextAlignment="Center"
|
||||
Margin="14,10,14,0"/>
|
||||
<controls:ExtendedCollectionView
|
||||
ItemsSource="{Binding LoginRequests}"
|
||||
ItemTemplate="{StaticResource loginRequestTemplate}"
|
||||
SelectionMode="Single"
|
||||
IsVisible="{Binding HasLoginRequests}"
|
||||
ExtraDataForLogging="Login requests page" >
|
||||
<controls:ExtendedCollectionView.Behaviors>
|
||||
<xct:EventToCommandBehavior
|
||||
EventName="SelectionChanged"
|
||||
Command="{Binding AnswerRequestCommand}"
|
||||
EventArgsConverter="{StaticResource SelectionChangedEventArgsConverter}" />
|
||||
</controls:ExtendedCollectionView.Behaviors>
|
||||
</controls:ExtendedCollectionView>
|
||||
</StackLayout>
|
||||
</RefreshView>
|
||||
<controls:IconLabelButton
|
||||
VerticalOptions="End"
|
||||
Margin="10,0"
|
||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
|
||||
Label="{u:I18n DeclineAllRequests}"
|
||||
ButtonCommand="{Binding DeclineAllRequestsCommand}"
|
||||
IsVisible="{Binding HasLoginRequests}"
|
||||
AutomationId="DeleteAllRequestsButton" />
|
||||
</StackLayout>
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentView
|
||||
x:Name="_mainContent">
|
||||
</ContentView>
|
||||
<Grid
|
||||
RowDefinitions="*, Auto"
|
||||
Padding="0, 10">
|
||||
<RefreshView
|
||||
Grid.Row="0"
|
||||
IsRefreshing="{Binding IsRefreshing}"
|
||||
Command="{Binding RefreshCommand}"
|
||||
VerticalOptions="Fill"
|
||||
BackgroundColor="{DynamicResource BackgroundColor}">
|
||||
<Grid RowDefinitions="Auto, *">
|
||||
<VerticalStackLayout Grid.Row="0"
|
||||
HorizontalOptions="Center">
|
||||
<Image
|
||||
x:Name="_emptyPlaceholder"
|
||||
Source="empty_login_requests"
|
||||
WidthRequest="160"
|
||||
HeightRequest="160"
|
||||
Margin="0,70,0,0"
|
||||
IsVisible="{Binding HasLoginRequests, Converter={StaticResource inverseBool}}"
|
||||
SemanticProperties.Description="{u:I18n NoPendingRequests}" />
|
||||
<controls:CustomLabel
|
||||
StyleClass="box-label-regular"
|
||||
Text="{u:I18n NoPendingRequests}"
|
||||
IsVisible="{Binding HasLoginRequests, Converter={StaticResource inverseBool}}"
|
||||
FontAttributes="{OnPlatform iOS=Bold}"
|
||||
FontWeight="500"
|
||||
Margin="14,10,14,0"/>
|
||||
</VerticalStackLayout>
|
||||
<controls:ExtendedCollectionView
|
||||
Grid.Row="1"
|
||||
ItemsSource="{Binding LoginRequests}"
|
||||
ItemTemplate="{StaticResource loginRequestTemplate}"
|
||||
SelectionMode="Single"
|
||||
IsVisible="{Binding HasLoginRequests}"
|
||||
ExtraDataForLogging="Login requests page" >
|
||||
<controls:ExtendedCollectionView.Behaviors>
|
||||
<xct:EventToCommandBehavior
|
||||
EventName="SelectionChanged"
|
||||
Command="{Binding AnswerRequestCommand}"
|
||||
EventArgsConverter="{StaticResource SelectionChangedEventArgsConverter}" />
|
||||
</controls:ExtendedCollectionView.Behaviors>
|
||||
</controls:ExtendedCollectionView>
|
||||
</Grid>
|
||||
</RefreshView>
|
||||
|
||||
<controls:IconLabelButton
|
||||
Grid.Row="1"
|
||||
VerticalOptions="End"
|
||||
Margin="10,0"
|
||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
|
||||
Label="{u:I18n DeclineAllRequests}"
|
||||
ButtonCommand="{Binding DeclineAllRequestsCommand}"
|
||||
IsVisible="{Binding HasLoginRequests}"
|
||||
AutomationId="DeleteAllRequestsButton" />
|
||||
|
||||
<Grid x:Name="_activityIndicatorGrid" Grid.Row="0" Grid.RowSpan="2" BackgroundColor="{DynamicResource BackgroundColor}">
|
||||
<ActivityIndicator IsRunning="True"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="Center" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Microsoft.Maui.Controls;
|
||||
@@ -19,7 +20,6 @@ namespace Bit.App.Pages
|
||||
public LoginPasswordlessRequestsListPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
SetActivityIndicator(_mainContent);
|
||||
_vm = BindingContext as LoginPasswordlessRequestsListViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
@@ -27,9 +27,21 @@ namespace Bit.App.Pages
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await LoadOnAppearedAsync(_mainLayout, false, _vm.RefreshAsync, _mainContent);
|
||||
try
|
||||
{
|
||||
_activityIndicatorGrid.IsVisible = true;
|
||||
|
||||
UpdatePlaceholder();
|
||||
await _vm.RefreshAsync();
|
||||
UpdatePlaceholder();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_activityIndicatorGrid.IsVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
|
||||
@@ -66,7 +66,6 @@ namespace Bit.App.Pages
|
||||
{
|
||||
try
|
||||
{
|
||||
IsRefreshing = true;
|
||||
LoginRequests.ReplaceRange(await _authService.GetActivePasswordlessLoginRequestsAsync());
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -108,7 +107,7 @@ namespace Bit.App.Pages
|
||||
Origin = loginRequestData.Origin
|
||||
});
|
||||
|
||||
await Device.InvokeOnMainThreadAsync(() => Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page)));
|
||||
await MainThread.InvokeOnMainThreadAsync(() => Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page)));
|
||||
}
|
||||
|
||||
private async Task DeclineAllRequestsAsync()
|
||||
|
||||
@@ -370,7 +370,7 @@ namespace Bit.App.Pages
|
||||
|
||||
if (!_supportsBiometric
|
||||
||
|
||||
!await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null))
|
||||
await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null) != true)
|
||||
{
|
||||
_canUnlockWithBiometrics = false;
|
||||
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithBiometrics)));
|
||||
|
||||
@@ -96,6 +96,14 @@ namespace Bit.App.Pages
|
||||
if (message.Command == "syncCompleted")
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(async () => await UpdateVaultButtonTitleAsync());
|
||||
try
|
||||
{
|
||||
await ForcePasswordResetIfNeededAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
await UpdateVaultButtonTitleAsync();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user