mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
Compare commits
120 Commits
temp-deleg
...
vault/pm-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
485aca083b | ||
|
|
2b78859d06 | ||
|
|
81205154c4 | ||
|
|
14d2660b61 | ||
|
|
bad5673724 | ||
|
|
67b76a777e | ||
|
|
02f5936fce | ||
|
|
fc208b08d7 | ||
|
|
290266c262 | ||
|
|
c40eb9d3db | ||
|
|
df9fcd1aca | ||
|
|
890eff79a2 | ||
|
|
265c95b494 | ||
|
|
f165135147 | ||
|
|
d458f17ad6 | ||
|
|
c3bd4b84b1 | ||
|
|
0af78d0e03 | ||
|
|
e86a01a7db | ||
|
|
e16074a73e | ||
|
|
a333e72448 | ||
|
|
ffb7b3b8ac | ||
|
|
c8d0db9f31 | ||
|
|
8566f5c00a | ||
|
|
25e394eb5e | ||
|
|
354aec09d9 | ||
|
|
4332e7a498 | ||
|
|
b65f18d8e2 | ||
|
|
7a3816007b | ||
|
|
477b1cca44 | ||
|
|
dee9524b2c | ||
|
|
f5572511c6 | ||
|
|
9a17da009c | ||
|
|
b5443c79d2 | ||
|
|
9dc620b492 | ||
|
|
1f966e6cbe | ||
|
|
a4fa03656e | ||
|
|
12385d9add | ||
|
|
13ca0fd4cb | ||
|
|
0b28b954fe | ||
|
|
e4841bb322 | ||
|
|
98621341a2 | ||
|
|
2023fe6644 | ||
|
|
567a23e29f | ||
|
|
8b65d99442 | ||
|
|
f80ec1b221 | ||
|
|
ba1183234b | ||
|
|
5946af9eec | ||
|
|
b091051633 | ||
|
|
06488539b0 | ||
|
|
8f77822b1b | ||
|
|
3c1105b35d | ||
|
|
c847449db8 | ||
|
|
c2771eb3c7 | ||
|
|
6e41731dcb | ||
|
|
7c90b35592 | ||
|
|
93f9dc4498 | ||
|
|
e033832261 | ||
|
|
fa5d92fbf7 | ||
|
|
e672cb132f | ||
|
|
e7a7eed7e8 | ||
|
|
43a4915323 | ||
|
|
b1ae3cc325 | ||
|
|
b9dada07ea | ||
|
|
58442389df | ||
|
|
a3378d33ae | ||
|
|
2e1982b08e | ||
|
|
e9e9b6f7bc | ||
|
|
9be8fec219 | ||
|
|
9db32ca019 | ||
|
|
f04ff7777a | ||
|
|
64775694e0 | ||
|
|
3c0007a21a | ||
|
|
35ff235010 | ||
|
|
01bd5a7b8d | ||
|
|
3fce8c76bc | ||
|
|
3b64d7b979 | ||
|
|
f343a2cdbb | ||
|
|
9a9fb85ad8 | ||
|
|
e7f9d64edb | ||
|
|
459d20c019 | ||
|
|
a8529fa4b7 | ||
|
|
d1e82c9f1d | ||
|
|
9bc2901255 | ||
|
|
e3441845cd | ||
|
|
3f463647a0 | ||
|
|
4f169a6fe3 | ||
|
|
82c2e91446 | ||
|
|
7482808857 | ||
|
|
fd233fa27f | ||
|
|
19f238d9bb | ||
|
|
6f6487ccc9 | ||
|
|
dd3dc82595 | ||
|
|
40c80f082d | ||
|
|
bca5b95446 | ||
|
|
602627b5fa | ||
|
|
6f32afb919 | ||
|
|
2ca47a4da4 | ||
|
|
4ff56ba11e | ||
|
|
22d0cc681c | ||
|
|
4e0a18cce5 | ||
|
|
c9fdfa7a15 | ||
|
|
850a7e754a | ||
|
|
67c5f79625 | ||
|
|
04e7cfe06d | ||
|
|
d6c2ebe4c2 | ||
|
|
2a28294f91 | ||
|
|
8584bbaecc | ||
|
|
2f3cded9c5 | ||
|
|
eff0ea7ce7 | ||
|
|
6c3a53dd76 | ||
|
|
cf8d801c55 | ||
|
|
eaa6844742 | ||
|
|
29e2f728e0 | ||
|
|
fe160a570f | ||
|
|
a508bea4b0 | ||
|
|
a73923c4f7 | ||
|
|
11465e8975 | ||
|
|
4c88524f0e | ||
|
|
f1c20e03bc | ||
|
|
920a2273c5 |
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
@@ -21,7 +21,6 @@ src/App/Platforms/iOS/Info.plist
|
|||||||
|
|
||||||
## Platform team files ##
|
## Platform team files ##
|
||||||
appIcons @bitwarden/team-platform-dev
|
appIcons @bitwarden/team-platform-dev
|
||||||
build.cake @bitwarden/team-platform-dev
|
|
||||||
|
|
||||||
## Vault team files ##
|
## Vault team files ##
|
||||||
src/watchOS @bitwarden/team-vault-dev
|
src/watchOS @bitwarden/team-vault-dev
|
||||||
@@ -30,14 +29,14 @@ src/watchOS @bitwarden/team-vault-dev
|
|||||||
src/Core/Services/EmailForwarders @bitwarden/team-tools-dev
|
src/Core/Services/EmailForwarders @bitwarden/team-tools-dev
|
||||||
|
|
||||||
## Crowdin Sync files ##
|
## 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
|
src/watchOS/bitwarden/bitwarden\ WatchKit\ Extension/Localization @bitwarden/team-tools-dev
|
||||||
store/apple @bitwarden/team-tools-dev
|
store/apple @bitwarden/team-tools-dev
|
||||||
store/google @bitwarden/team-tools-dev
|
store/google @bitwarden/team-tools-dev
|
||||||
|
|
||||||
## Locales ##
|
## Locales ##
|
||||||
src/App/Resources/AppResources.Designer.cs
|
src/Core/Resources/Localization/AppResources.Designer.cs
|
||||||
src/App/Resources/AppResources.resx
|
src/Core/Resources/Localization/AppResources.resx
|
||||||
src/watchOS/bitwarden/bitwarden\ WatchKit\ Extension/Localization/en.lproj
|
src/watchOS/bitwarden/bitwarden\ WatchKit\ Extension/Localization/en.lproj
|
||||||
store/apple/en
|
store/apple/en
|
||||||
store/google/en
|
store/google/en
|
||||||
|
|||||||
35
.github/labeler.yml
vendored
35
.github/labeler.yml
vendored
@@ -1,19 +1,26 @@
|
|||||||
android:
|
android:
|
||||||
- src/App/*
|
- changed-files:
|
||||||
- src/Core/*
|
- any-glob-to-any-file:
|
||||||
- src/Android/*
|
- src/App/*
|
||||||
|
- src/Core/*
|
||||||
|
- src/Android/*
|
||||||
|
- 'src/Xamarin.AndroidX.Credentials/*'
|
||||||
|
|
||||||
iOS:
|
iOS:
|
||||||
- src/App/*
|
- changed-files:
|
||||||
- src/Core/*
|
- any-glob-to-any-file:
|
||||||
- lib/ios/*
|
- src/App/*
|
||||||
- src/iOS/*
|
- src/Core/*
|
||||||
- 'src/iOS.Autofill/*'
|
- lib/ios/*
|
||||||
- 'src/iOS.Core/*'
|
- src/iOS/*
|
||||||
- 'src/iOS.Extension/*'
|
- 'src/iOS.Autofill/*'
|
||||||
- 'src/iOS.ShareExtension/*'
|
- 'src/iOS.Core/*'
|
||||||
- 'src/iOS.Widget/*'
|
- 'src/iOS.Extension/*'
|
||||||
- src/watchOS/*
|
- 'src/iOS.ShareExtension/*'
|
||||||
|
- 'src/iOS.Widget/*'
|
||||||
|
- src/watchOS/*
|
||||||
|
|
||||||
watchOS:
|
watchOS:
|
||||||
- src/watchOS/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- src/watchOS/*
|
||||||
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.
@@ -7,7 +7,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
close-issue:
|
close-issue:
|
||||||
name: 'Close issue with automatic response'
|
name: 'Close issue with automatic response'
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
350
.github/workflows/build-beta.yml
vendored
Normal file
350
.github/workflows/build-beta.yml
vendored
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
---
|
||||||
|
name: Build Beta
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
ref:
|
||||||
|
description: 'Branch or tag to build'
|
||||||
|
required: true
|
||||||
|
default: 'main'
|
||||||
|
type: string
|
||||||
|
|
||||||
|
env:
|
||||||
|
main_app_folder_path: src/App
|
||||||
|
main_app_project_path: src/App/App.csproj
|
||||||
|
target-net-version: net8.0
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
setup:
|
||||||
|
name: Setup
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
outputs:
|
||||||
|
rc_branch_exists: ${{ steps.branch-check.outputs.rc_branch_exists }}
|
||||||
|
hotfix_branch_exists: ${{ steps.branch-check.outputs.hotfix_branch_exists }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
|
with:
|
||||||
|
submodules: 'true'
|
||||||
|
|
||||||
|
- name: Check if special branches exist
|
||||||
|
id: branch-check
|
||||||
|
run: |
|
||||||
|
if [[ $(git ls-remote --heads origin rc) ]]; then
|
||||||
|
echo "rc_branch_exists=1" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "rc_branch_exists=0" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $(git ls-remote --heads origin hotfix-rc) ]]; then
|
||||||
|
echo "hotfix_branch_exists=1" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "hotfix_branch_exists=0" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
ios:
|
||||||
|
name: Apple iOS
|
||||||
|
runs-on: macos-14
|
||||||
|
needs: setup
|
||||||
|
env:
|
||||||
|
ios_folder_path: src/App/Platforms/iOS
|
||||||
|
app_output_name: App
|
||||||
|
app_ci_output_filename: App_x64_Debug
|
||||||
|
steps:
|
||||||
|
- name: Set XCode version
|
||||||
|
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
|
||||||
|
with:
|
||||||
|
xcode-version: 15.1
|
||||||
|
|
||||||
|
- name: Setup NuGet
|
||||||
|
uses: nuget/setup-nuget@a21f25cd3998bf370fde17e3f1b4c12c175172f9 # v2.0.0
|
||||||
|
with:
|
||||||
|
nuget-version: 6.4.0
|
||||||
|
|
||||||
|
- name: Set up .NET
|
||||||
|
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
||||||
|
with:
|
||||||
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
- name: Print environment
|
||||||
|
run: |
|
||||||
|
nuget help | grep Version
|
||||||
|
dotnet --info
|
||||||
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ inputs.ref }}
|
||||||
|
submodules: 'true'
|
||||||
|
|
||||||
|
- 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: "appcenter-ios-token"
|
||||||
|
|
||||||
|
- name: Download Provisioning Profiles secrets
|
||||||
|
env:
|
||||||
|
ACCOUNT_NAME: bitwardenci
|
||||||
|
CONTAINER_NAME: profiles
|
||||||
|
run: |
|
||||||
|
mkdir -p $HOME/secrets
|
||||||
|
profiles=(
|
||||||
|
"dist_beta_autofill.mobileprovision"
|
||||||
|
"dist_beta_bitwarden.mobileprovision"
|
||||||
|
"dist_beta_extension.mobileprovision"
|
||||||
|
"dist_beta_share_extension.mobileprovision"
|
||||||
|
"dist_beta_bitwarden_watch_app.mobileprovision"
|
||||||
|
"dist_beta_bitwarden_watch_app_extension.mobileprovision"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 $HOME/secrets/$FILE --output none
|
||||||
|
|
||||||
|
- name: Increment version
|
||||||
|
run: |
|
||||||
|
BUILD_NUMBER=$((100 + $GITHUB_RUN_NUMBER))
|
||||||
|
echo "##### Setting CFBundleVersion $BUILD_NUMBER"
|
||||||
|
|
||||||
|
echo "### CFBundleVersion $BUILD_NUMBER" >> $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
|
||||||
|
|
||||||
|
- name: Update Entitlements
|
||||||
|
run: |
|
||||||
|
echo "##### Updating Entitlements"
|
||||||
|
perl -0777 -pi.bak -e 's/<key>aps-environment<\/key>\s*<string>development<\/string>/<key>aps-environment<\/key>\n\t<string>beta<\/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 $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
|
||||||
|
run: |
|
||||||
|
AUTOFILL_PROFILE_PATH=$HOME/secrets/dist_beta_autofill.mobileprovision
|
||||||
|
BITWARDEN_PROFILE_PATH=$HOME/secrets/dist_beta_bitwarden.mobileprovision
|
||||||
|
EXTENSION_PROFILE_PATH=$HOME/secrets/dist_beta_extension.mobileprovision
|
||||||
|
SHARE_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_beta_share_extension.mobileprovision
|
||||||
|
WATCH_APP_PROFILE_PATH=$HOME/secrets/dist_beta_bitwarden_watch_app.mobileprovision
|
||||||
|
WATCH_APP_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_beta_bitwarden_watch_app_extension.mobileprovision
|
||||||
|
PROFILES_DIR_PATH=$HOME/Library/MobileDevice/Provisioning\ Profiles
|
||||||
|
|
||||||
|
mkdir -p "$PROFILES_DIR_PATH"
|
||||||
|
|
||||||
|
AUTOFILL_UUID=$(grep UUID -A1 -a $AUTOFILL_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
|
||||||
|
cp $AUTOFILL_PROFILE_PATH "$PROFILES_DIR_PATH/$AUTOFILL_UUID.mobileprovision"
|
||||||
|
|
||||||
|
BITWARDEN_UUID=$(grep UUID -A1 -a $BITWARDEN_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
|
||||||
|
cp $BITWARDEN_PROFILE_PATH "$PROFILES_DIR_PATH/$BITWARDEN_UUID.mobileprovision"
|
||||||
|
|
||||||
|
EXTENSION_UUID=$(grep UUID -A1 -a $EXTENSION_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
|
||||||
|
cp $EXTENSION_PROFILE_PATH "$PROFILES_DIR_PATH/$EXTENSION_UUID.mobileprovision"
|
||||||
|
|
||||||
|
SHARE_EXTENSION_UUID=$(grep UUID -A1 -a $SHARE_EXTENSION_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
|
||||||
|
cp $SHARE_EXTENSION_PROFILE_PATH "$PROFILES_DIR_PATH/$SHARE_EXTENSION_UUID.mobileprovision"
|
||||||
|
|
||||||
|
WATCH_APP_UUID=$(grep UUID -A1 -a $WATCH_APP_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
|
||||||
|
cp $WATCH_APP_PROFILE_PATH "$PROFILES_DIR_PATH/$WATCH_APP_UUID.mobileprovision"
|
||||||
|
|
||||||
|
WATCH_APP_EXTENSION_UUID=$(grep UUID -A1 -a $WATCH_APP_EXTENSION_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
|
||||||
|
cp $WATCH_APP_EXTENSION_PROFILE_PATH "$PROFILES_DIR_PATH/$WATCH_APP_EXTENSION_UUID.mobileprovision"
|
||||||
|
|
||||||
|
- name: Restore packages
|
||||||
|
run: |
|
||||||
|
dotnet restore
|
||||||
|
dotnet tool restore
|
||||||
|
|
||||||
|
- name: Setup iOS build CAKE (Testing)
|
||||||
|
run: dotnet cake build.cake --target iOS --variant beta
|
||||||
|
|
||||||
|
- name: Bulid WatchApp
|
||||||
|
run: |
|
||||||
|
echo "##### Build WatchApp with Release Configuration"
|
||||||
|
xcodebuild archive -workspace ./src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace -configuration Release -scheme bitwarden\ WatchKit\ App -archivePath ./src/watchOS/bitwarden
|
||||||
|
|
||||||
|
echo "##### Done"
|
||||||
|
|
||||||
|
- name: Archive Build for App Store
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
Write-Output "##### 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 "##### Done"
|
||||||
|
|
||||||
|
- name: Archive Build for Mobile Automation
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
Write-Output "##### 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 "##### Done"
|
||||||
|
ls ~/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: |
|
||||||
|
ARCHIVE_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive"
|
||||||
|
|
||||||
|
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: |
|
||||||
|
zip -r -q ${{ env.app_ci_output_filename }}.app.zip $ARCHIVE_PATH
|
||||||
|
mv ${{ env.app_ci_output_filename }}.app.zip $EXPORT_PATH
|
||||||
|
|
||||||
|
- name: Show Bitwarden Export
|
||||||
|
shell: bash
|
||||||
|
run: ls -a -R ./bitwarden-export
|
||||||
|
|
||||||
|
- 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"
|
||||||
|
|
||||||
|
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@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||||
|
with:
|
||||||
|
name: Bitwarden iOS
|
||||||
|
path: |
|
||||||
|
./bitwarden-export/Bitwarden*.ipa
|
||||||
|
./bitwarden-export/dSYMs/*.*
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload .app file for Automation CI
|
||||||
|
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||||
|
with:
|
||||||
|
name: ${{ env.app_ci_output_filename }}.app.zip
|
||||||
|
path: ./bitwarden-export/${{ env.app_ci_output_filename }}.app.zip
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Install AppCenter CLI
|
||||||
|
run: npm install -g appcenter-cli
|
||||||
|
|
||||||
|
- name: Upload dSYMs to App Center
|
||||||
|
env:
|
||||||
|
APPCENTER_IOS_TOKEN: ${{ steps.retrieve-secrets.outputs.appcenter-ios-token }}
|
||||||
|
run: appcenter crashes upload-symbols -a bitwarden/bitwarden -s "./bitwarden-export/dSYMs" --token $APPCENTER_IOS_TOKEN
|
||||||
|
|
||||||
|
- name: Upload Watch dSYMs to Firebase Crashlytics
|
||||||
|
run: |
|
||||||
|
echo "##### Uploading Watch dSYMs to Firebase"
|
||||||
|
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
|
||||||
|
env:
|
||||||
|
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
||||||
|
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
xcrun altool --validate-app --type ios --file "./bitwarden-export/Bitwarden Beta.ipa" \
|
||||||
|
--username "$APPLE_ID_USERNAME" --password "$APPLE_ID_PASSWORD"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Deploy to App Store
|
||||||
|
env:
|
||||||
|
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
||||||
|
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
xcrun altool --upload-app --type ios --file "./bitwarden-export/Bitwarden Beta.ipa" \
|
||||||
|
--username "$APPLE_ID_USERNAME" --password "$APPLE_ID_PASSWORD"
|
||||||
|
|
||||||
|
check-failures:
|
||||||
|
name: Check for failures
|
||||||
|
if: always()
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs:
|
||||||
|
- setup
|
||||||
|
- ios
|
||||||
|
steps:
|
||||||
|
- name: Check if any job failed
|
||||||
|
if: |
|
||||||
|
(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
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
|
- name: Retrieve secrets
|
||||||
|
id: retrieve-secrets
|
||||||
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
keyvault: "bitwarden-ci"
|
||||||
|
secrets: "devops-alerts-slack-webhook-url"
|
||||||
|
|
||||||
|
- name: Notify Slack on failure
|
||||||
|
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
|
||||||
|
if: failure()
|
||||||
|
env:
|
||||||
|
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
||||||
|
with:
|
||||||
|
status: ${{ job.status }}
|
||||||
437
.github/workflows/build.yml
vendored
437
.github/workflows/build.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
|
|
||||||
- name: Set up CLOC
|
- name: Set up CLOC
|
||||||
run: |
|
run: |
|
||||||
@@ -31,6 +31,7 @@ jobs:
|
|||||||
- name: Print lines of code
|
- name: Print lines of code
|
||||||
run: cloc --vcs git --exclude-dir Resources,store,test,Properties --include-lang C#,XAML
|
run: cloc --vcs git --exclude-dir Resources,store,test,Properties --include-lang C#,XAML
|
||||||
|
|
||||||
|
|
||||||
setup:
|
setup:
|
||||||
name: Setup
|
name: Setup
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
@@ -39,7 +40,7 @@ jobs:
|
|||||||
hotfix_branch_exists: ${{ steps.branch-check.outputs.hotfix_branch_exists }}
|
hotfix_branch_exists: ${{ steps.branch-check.outputs.hotfix_branch_exists }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ jobs:
|
|||||||
echo "hotfix_branch_exists=0" >> $GITHUB_OUTPUT
|
echo "hotfix_branch_exists=0" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
android:
|
android:
|
||||||
name: Android
|
name: Android
|
||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
@@ -67,10 +69,11 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
variant: ["prod", "qa"]
|
variant: ["prod", "qa"]
|
||||||
env:
|
env:
|
||||||
android_folder_path: src/App/Platforms/Android
|
android_folder_path: src\App\Platforms\Android
|
||||||
|
android_folder_path_bash: src/App/Platforms/Android
|
||||||
steps:
|
steps:
|
||||||
- name: Setup NuGet
|
- name: Setup NuGet
|
||||||
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
|
uses: nuget/setup-nuget@a21f25cd3998bf370fde17e3f1b4c12c175172f9 # v2.0.0
|
||||||
with:
|
with:
|
||||||
nuget-version: 6.4.0
|
nuget-version: 6.4.0
|
||||||
|
|
||||||
@@ -80,9 +83,9 @@ jobs:
|
|||||||
dotnet-version: '8.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
- name: Set up MSBuild
|
- name: Set up MSBuild
|
||||||
uses: microsoft/setup-msbuild@ede762b26a2de8d110bb5a3db4d7e0e080c0e917 # v1.3.3
|
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2.0.0
|
||||||
|
|
||||||
# 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.
|
# are starting to come pre-installed on the GH Actions build agents.
|
||||||
- name: Install MAUI Workload
|
- name: Install MAUI Workload
|
||||||
run: dotnet workload install maui --ignore-failed-sources
|
run: dotnet workload install maui --ignore-failed-sources
|
||||||
@@ -93,7 +96,8 @@ jobs:
|
|||||||
- name: Install Microsoft OpenJDK 11
|
- name: Install Microsoft OpenJDK 11
|
||||||
run: |
|
run: |
|
||||||
choco install microsoft-openjdk11 --no-progress
|
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"
|
Write-Output "Java Home: $env:JAVA_HOME"
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
@@ -105,43 +109,47 @@ jobs:
|
|||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Decrypt secrets
|
- name: Login to Azure - CI Subscription
|
||||||
env:
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
with:
|
||||||
run: |
|
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
mkdir -p ~/secrets
|
|
||||||
|
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
- name: Download secrets
|
||||||
--output ./${{ env.main_app_folder_path }}/app_play-keystore.jks ./.github/secrets/app_play-keystore.jks.gpg
|
env:
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
ACCOUNT_NAME: bitwardenci
|
||||||
--output ./${{ env.main_app_folder_path }}/app_upload-keystore.jks ./.github/secrets/app_upload-keystore.jks.gpg
|
CONTAINER_NAME: mobile
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
run: |
|
||||||
--output $HOME/secrets/play_creds.json ./.github/secrets/play_creds.json.gpg
|
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
|
shell: bash
|
||||||
|
|
||||||
- name: Decrypt secrets - Google Services
|
- name: Download secrets - Google Services
|
||||||
if: ${{ matrix.variant == 'prod' }}
|
if: ${{ matrix.variant == 'prod' }}
|
||||||
env:
|
env:
|
||||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
ACCOUNT_NAME: bitwardenci
|
||||||
|
CONTAINER_NAME: mobile
|
||||||
run: |
|
run: |
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||||
--output ./${{ env.android_folder_path }}/google-services.json ./.github/secrets/google-services.json.gpg
|
--name google-services.json --file ./${{ env.android_folder_path_bash }}/google-services.json --output none
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Increment version
|
- name: Increment version
|
||||||
run: |
|
run: |
|
||||||
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
|
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\"/" \
|
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
|
shell: bash
|
||||||
|
|
||||||
- name: Restore packages
|
- name: Restore packages
|
||||||
@@ -150,83 +158,75 @@ jobs:
|
|||||||
- name: Restore tools
|
- name: Restore tools
|
||||||
run: dotnet tool restore
|
run: dotnet tool restore
|
||||||
|
|
||||||
# - name: Verify Format
|
# - name: Run Core tests
|
||||||
# run: dotnet tool run dotnet-format --check
|
# run: |
|
||||||
|
# dotnet test test/Core.Test/Core.Test.csproj --logger "trx;LogFileName=test-results.trx" `
|
||||||
|
# /p:CustomConstants=UT
|
||||||
|
|
||||||
- name: Run Core tests
|
# - name: Report test results
|
||||||
run: dotnet test test/Core.Test/Core.Test.csproj --logger "trx;LogFileName=test-results.trx" /p:CustomConstants=UT
|
# uses: dorny/test-reporter@eaa763f6ffc21c7a37837f56cd5f9737f27fc6c8 # v1.8.0
|
||||||
|
# if: always()
|
||||||
- name: Report test results
|
# with:
|
||||||
uses: dorny/test-reporter@eaa763f6ffc21c7a37837f56cd5f9737f27fc6c8 # v1.8.0
|
# name: Test Results
|
||||||
if: always()
|
# path: "**/test-results.trx"
|
||||||
with:
|
# reporter: dotnet-trx
|
||||||
name: Test Results
|
# fail-on-error: true
|
||||||
path: "**/test-results.trx"
|
|
||||||
reporter: dotnet-trx
|
|
||||||
fail-on-error: true
|
|
||||||
|
|
||||||
- name: Build Play Store publisher
|
- name: Build Play Store publisher
|
||||||
if: ${{ matrix.variant == 'prod' }}
|
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 }})
|
- name: Setup Android build (${{ matrix.variant }})
|
||||||
run: dotnet cake build.cake --target Android --variant ${{ matrix.variant }}
|
run: dotnet cake build.cake --target Android --variant ${{ matrix.variant }}
|
||||||
|
|
||||||
- name: Build Android
|
- name: Build & Sign 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
|
|
||||||
env:
|
env:
|
||||||
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
|
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
|
||||||
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
|
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
$projToBuild = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}");
|
$projToBuild = "$($env:GITHUB_WORKSPACE)/${{ env.main_app_project_path }}";
|
||||||
$packageName = "com.x8bit.bitwarden";
|
$packageName = "com.x8bit.bitwarden";
|
||||||
|
|
||||||
if ("${{ matrix.variant }}" -ne "prod")
|
if ("${{ matrix.variant }}" -ne "prod")
|
||||||
{
|
{
|
||||||
$packageName = "com.x8bit.bitwarden.${{ matrix.variant }}";
|
$packageName = "com.x8bit.bitwarden.${{ matrix.variant }}";
|
||||||
}
|
}
|
||||||
Write-Output "########################################"
|
|
||||||
Write-Output "##### Sign Google Play Bundle Release Configuration"
|
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 "##### 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");
|
$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");
|
$signedAabDestPath = "$($env:GITHUB_WORKSPACE)\$($packageName).aab";
|
||||||
Copy-Item $signedAabPath $signedAabDestPath
|
Copy-Item $signedAabPath $signedAabDestPath
|
||||||
|
|
||||||
Write-Output "########################################"
|
|
||||||
Write-Output "##### Sign APK Release Configuration"
|
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 "##### 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
|
Copy-Item $signedApkPath $signedApkDestPath
|
||||||
|
|
||||||
- name: Upload Prod .aab artifact
|
- name: Upload Prod .aab artifact
|
||||||
if: ${{ matrix.variant == 'prod' }}
|
if: ${{ matrix.variant == 'prod' }}
|
||||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||||
with:
|
with:
|
||||||
name: com.x8bit.bitwarden.aab
|
name: com.x8bit.bitwarden.aab
|
||||||
path: ./com.x8bit.bitwarden.aab
|
path: ./com.x8bit.bitwarden.aab
|
||||||
@@ -234,7 +234,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Prod .apk artifact
|
- name: Upload Prod .apk artifact
|
||||||
if: ${{ matrix.variant == 'prod' }}
|
if: ${{ matrix.variant == 'prod' }}
|
||||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||||
with:
|
with:
|
||||||
name: com.x8bit.bitwarden.apk
|
name: com.x8bit.bitwarden.apk
|
||||||
path: ./com.x8bit.bitwarden.apk
|
path: ./com.x8bit.bitwarden.apk
|
||||||
@@ -242,7 +242,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Other .apk artifact
|
- name: Upload Other .apk artifact
|
||||||
if: ${{ matrix.variant != 'prod' }}
|
if: ${{ matrix.variant != 'prod' }}
|
||||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||||
with:
|
with:
|
||||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||||
path: ./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
|
- name: Upload .apk sha file for prod
|
||||||
if: ${{ matrix.variant == 'prod' }}
|
if: ${{ matrix.variant == 'prod' }}
|
||||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||||
with:
|
with:
|
||||||
name: bw-android-apk-sha256.txt
|
name: bw-android-apk-sha256.txt
|
||||||
path: ./bw-android-apk-sha256.txt
|
path: ./bw-android-apk-sha256.txt
|
||||||
@@ -270,7 +270,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload .apk sha file for other
|
- name: Upload .apk sha file for other
|
||||||
if: ${{ matrix.variant != 'prod' }}
|
if: ${{ matrix.variant != 'prod' }}
|
||||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||||
with:
|
with:
|
||||||
name: bw-android-${{ matrix.variant }}-apk-sha256.txt
|
name: bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||||
path: ./bw-android-${{ matrix.variant }}-apk-sha256.txt
|
path: ./bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||||
@@ -283,36 +283,36 @@ jobs:
|
|||||||
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
|| github.ref == 'refs/heads/hotfix-rc' ) }}
|
|| github.ref == 'refs/heads/hotfix-rc' ) }}
|
||||||
run: |
|
run: |
|
||||||
PUBLISHER_PATH="$GITHUB_WORKSPACE/store/google/Publisher/bin/Release/net7.0/Publisher.dll"
|
$publisherPath = "$($env:GITHUB_WORKSPACE)\store\google\Publisher\bin\Release\net8.0\Publisher.dll"
|
||||||
CREDS_PATH="$HOME/secrets/play_creds.json"
|
$credsPath = "$($HOME)\secrets\play_creds.json"
|
||||||
AAB_PATH="$GITHUB_WORKSPACE/com.x8bit.bitwarden.aab"
|
$aabPath = "$($env:GITHUB_WORKSPACE)\com.x8bit.bitwarden.aab"
|
||||||
TRACK="internal"
|
$track = "internal"
|
||||||
|
|
||||||
dotnet $PUBLISHER_PATH $CREDS_PATH $AAB_PATH $TRACK
|
dotnet $publisherPath $credsPath $aabPath $track
|
||||||
shell: bash
|
|
||||||
|
|
||||||
|
|
||||||
f-droid:
|
f-droid:
|
||||||
name: F-Droid Build
|
name: F-Droid Build
|
||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
env:
|
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
|
android_manifest_path: src/App/Platforms/Android/AndroidManifest.xml
|
||||||
steps:
|
steps:
|
||||||
- name: Setup NuGet
|
- name: Setup NuGet
|
||||||
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
|
uses: nuget/setup-nuget@a21f25cd3998bf370fde17e3f1b4c12c175172f9 # v2.0.0
|
||||||
with:
|
with:
|
||||||
nuget-version: 6.4.0
|
nuget-version: 6.4.0
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
- name: Set up MSBuild
|
- name: Set up MSBuild
|
||||||
uses: microsoft/setup-msbuild@ede762b26a2de8d110bb5a3db4d7e0e080c0e917 # v1.3.3
|
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2.0.0
|
||||||
|
|
||||||
# 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.
|
# are starting to come pre-installed on the GH Actions build agents.
|
||||||
- name: Install MAUI Workload
|
- name: Install MAUI Workload
|
||||||
run: dotnet workload install maui --ignore-failed-sources
|
run: dotnet workload install maui --ignore-failed-sources
|
||||||
@@ -335,25 +335,27 @@ jobs:
|
|||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
|
|
||||||
- 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:
|
env:
|
||||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
ACCOUNT_NAME: bitwardenci
|
||||||
|
CONTAINER_NAME: mobile
|
||||||
|
FILE: app_fdroid-keystore.jks
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/secrets
|
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME --name $FILE \
|
||||||
|
--file ${{ env.android_folder_path_bash }}/$FILE --output none
|
||||||
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
|
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Increment version
|
- name: Increment version
|
||||||
run: |
|
run: |
|
||||||
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
|
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
|
||||||
|
echo "##### Setting F-Droid 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\"/" \
|
sed -i "s/android:versionCode=\"1\"/android:versionCode=\"$BUILD_NUMBER\"/" \
|
||||||
./${{ env.android_manifest_path }}
|
./${{ env.android_manifest_path }}
|
||||||
@@ -361,21 +363,16 @@ jobs:
|
|||||||
|
|
||||||
- name: Clean for F-Droid
|
- name: Clean for F-Droid
|
||||||
run: |
|
run: |
|
||||||
$appPath = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}");
|
$directoryBuildProps = $($env:GITHUB_WORKSPACE + "/Directory.Build.props");
|
||||||
$corePath = $($env:GITHUB_WORKSPACE + "/src/Core/Core.csproj");
|
|
||||||
|
|
||||||
$androidManifest = $($env:GITHUB_WORKSPACE + "/${{ env.android_manifest_path }}");
|
$androidManifest = $($env:GITHUB_WORKSPACE + "/${{ env.android_manifest_path }}");
|
||||||
|
|
||||||
Write-Output "########################################"
|
Write-Output "##### Back up project files"
|
||||||
Write-Output "##### Backup project files"
|
|
||||||
Write-Output "########################################"
|
|
||||||
|
|
||||||
Copy-Item $androidManifest $($androidManifest + ".original");
|
Copy-Item $androidManifest $($androidManifest + ".original");
|
||||||
Copy-Item $appPath $($appPath + ".original");
|
Copy-Item $directoryBuildProps $($directoryBuildProps + ".original");
|
||||||
|
|
||||||
Write-Output "########################################"
|
|
||||||
Write-Output "##### Cleanup Android Manifest"
|
Write-Output "##### Cleanup Android Manifest"
|
||||||
Write-Output "########################################"
|
|
||||||
|
|
||||||
$xml=New-Object XML;
|
$xml=New-Object XML;
|
||||||
$xml.Load($androidManifest);
|
$xml.Load($androidManifest);
|
||||||
@@ -385,44 +382,39 @@ jobs:
|
|||||||
|
|
||||||
$xml.Save($androidManifest);
|
$xml.Save($androidManifest);
|
||||||
|
|
||||||
|
Write-Output "##### Enabling FDROID constant"
|
||||||
|
|
||||||
|
(Get-Content $directoryBuildProps).Replace('<!-- <CustomConstants>FDROID</CustomConstants> -->', '<CustomConstants>FDROID</CustomConstants>') | Set-Content $directoryBuildProps
|
||||||
|
|
||||||
- name: Restore packages
|
- name: Restore packages
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
|
|
||||||
- name: Build for F-Droid
|
- name: Build & Sign 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
|
|
||||||
env:
|
env:
|
||||||
FDROID_KEYSTORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }}
|
FDROID_KEYSTORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
$projToBuild = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}");
|
$projToBuild = "$($env:GITHUB_WORKSPACE)\${{ env.main_app_project_path }}";
|
||||||
$packageName = "com.x8bit.bitwarden";
|
$packageName = "com.x8bit.bitwarden";
|
||||||
|
|
||||||
Write-Output "########################################"
|
|
||||||
Write-Output "##### Sign FDroid"
|
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 "##### 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");
|
$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");
|
$signedApkDestPath = "$($env:GITHUB_WORKSPACE)\com.x8bit.bitwarden-fdroid.apk";
|
||||||
|
|
||||||
Copy-Item $signedApkPath $signedApkDestPath
|
Copy-Item $signedApkPath $signedApkDestPath
|
||||||
|
|
||||||
- name: Upload F-Droid .apk artifact
|
- name: Upload F-Droid .apk artifact
|
||||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||||
with:
|
with:
|
||||||
name: com.x8bit.bitwarden-fdroid.apk
|
name: com.x8bit.bitwarden-fdroid.apk
|
||||||
path: ./com.x8bit.bitwarden-fdroid.apk
|
path: ./com.x8bit.bitwarden-fdroid.apk
|
||||||
@@ -434,15 +426,16 @@ jobs:
|
|||||||
-t sha256 | Out-File -Encoding ASCII ./bw-fdroid-apk-sha256.txt
|
-t sha256 | Out-File -Encoding ASCII ./bw-fdroid-apk-sha256.txt
|
||||||
|
|
||||||
- name: Upload F-Droid sha file
|
- name: Upload F-Droid sha file
|
||||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||||
with:
|
with:
|
||||||
name: bw-fdroid-apk-sha256.txt
|
name: bw-fdroid-apk-sha256.txt
|
||||||
path: ./bw-fdroid-apk-sha256.txt
|
path: ./bw-fdroid-apk-sha256.txt
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
ios:
|
ios:
|
||||||
name: Apple iOS
|
name: Apple iOS
|
||||||
runs-on: macos-13
|
runs-on: macos-14
|
||||||
needs: setup
|
needs: setup
|
||||||
env:
|
env:
|
||||||
ios_folder_path: src/App/Platforms/iOS
|
ios_folder_path: src/App/Platforms/iOS
|
||||||
@@ -455,16 +448,16 @@ jobs:
|
|||||||
xcode-version: 15.1
|
xcode-version: 15.1
|
||||||
|
|
||||||
- name: Setup NuGet
|
- name: Setup NuGet
|
||||||
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
|
uses: nuget/setup-nuget@a21f25cd3998bf370fde17e3f1b4c12c175172f9 # v2.0.0
|
||||||
with:
|
with:
|
||||||
nuget-version: 6.4.0
|
nuget-version: 6.4.0
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
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.
|
# are starting to come pre-installed on the GH Actions build agents.
|
||||||
- name: Install MAUI Workload
|
- name: Install MAUI Workload
|
||||||
run: dotnet workload install maui --ignore-failed-sources
|
run: dotnet workload install maui --ignore-failed-sources
|
||||||
@@ -477,7 +470,7 @@ jobs:
|
|||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
|
|
||||||
@@ -493,73 +486,71 @@ jobs:
|
|||||||
keyvault: "bitwarden-ci"
|
keyvault: "bitwarden-ci"
|
||||||
secrets: "appcenter-ios-token"
|
secrets: "appcenter-ios-token"
|
||||||
|
|
||||||
- name: Decrypt secrets
|
- name: Download Provisioning Profiles secrets
|
||||||
env:
|
env:
|
||||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
ACCOUNT_NAME: bitwardenci
|
||||||
|
CONTAINER_NAME: profiles
|
||||||
run: |
|
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" \
|
for FILE in "${profiles[@]}"
|
||||||
--output $HOME/secrets/bitwarden-mobile-key.p12 ./.github/secrets/bitwarden-mobile-key.p12.gpg
|
do
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME --name $FILE \
|
||||||
--output $HOME/secrets/iphone-distribution-cert.p12 ./.github/secrets/iphone-distribution-cert.p12.gpg
|
--file $HOME/secrets/$FILE --output none
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
done
|
||||||
--output $HOME/secrets/dist_autofill.mobileprovision ./.github/secrets/dist_autofill.mobileprovision.gpg
|
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
- name: Download Google Services secret
|
||||||
--output $HOME/secrets/dist_bitwarden.mobileprovision ./.github/secrets/dist_bitwarden.mobileprovision.gpg
|
env:
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
ACCOUNT_NAME: bitwardenci
|
||||||
--output $HOME/secrets/dist_extension.mobileprovision ./.github/secrets/dist_extension.mobileprovision.gpg
|
CONTAINER_NAME: mobile
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
FILE: GoogleService-Info.plist
|
||||||
--output $HOME/secrets/dist_share_extension.mobileprovision \
|
run: |
|
||||||
./.github/secrets/dist_share_extension.mobileprovision.gpg
|
mkdir -p $HOME/secrets
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME --name $FILE \
|
||||||
--output $HOME/secrets/dist_watch_app.mobileprovision \
|
--file src/watchOS/bitwarden/$FILE --output none
|
||||||
./.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
|
|
||||||
|
|
||||||
- name: Increment version
|
- name: Increment version
|
||||||
run: |
|
run: |
|
||||||
BUILD_NUMBER=$((100 + $GITHUB_RUN_NUMBER))
|
BUILD_NUMBER=$((100 + $GITHUB_RUN_NUMBER))
|
||||||
|
echo "##### Setting iOS CFBundleVersion to $BUILD_NUMBER" | tee -a $GITHUB_STEP_SUMMARY
|
||||||
echo "########################################"
|
|
||||||
echo "##### Setting CFBundleVersion $BUILD_NUMBER"
|
|
||||||
echo "########################################"
|
|
||||||
|
|
||||||
echo "### CFBundleVersion $BUILD_NUMBER" >> $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>/' ./${{ 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.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.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
|
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
|
cd src/watchOS/bitwarden
|
||||||
agvtool new-version -all $BUILD_NUMBER
|
agvtool new-version -all $BUILD_NUMBER
|
||||||
|
|
||||||
- name: Update Entitlements
|
- name: Update Entitlements
|
||||||
run: |
|
run: |
|
||||||
echo "########################################"
|
|
||||||
echo "##### Updating Entitlements"
|
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
|
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
|
- name: Set up Keychain
|
||||||
env:
|
env:
|
||||||
KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
|
KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
|
||||||
MOBILE_KEY_PASSWORD: ${{ secrets.IOS_KEY_PASSWORD }}
|
|
||||||
DIST_CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
|
|
||||||
run: |
|
run: |
|
||||||
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
||||||
security default-keychain -s build.keychain
|
security default-keychain -s build.keychain
|
||||||
security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
||||||
security set-keychain-settings -lut 1200 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 $HOME/certificates/ios-distribution.p12 -k build.keychain -P "" -T /usr/bin/codesign \
|
||||||
security import ~/secrets/iphone-distribution-cert.p12 -k build.keychain -P $DIST_CERT_PASSWORD \
|
-T /usr/bin/security
|
||||||
-T /usr/bin/codesign -T /usr/bin/security
|
|
||||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
|
||||||
|
|
||||||
- name: Set up provisioning profiles
|
- name: Set up provisioning profiles
|
||||||
@@ -568,8 +559,8 @@ jobs:
|
|||||||
BITWARDEN_PROFILE_PATH=$HOME/secrets/dist_bitwarden.mobileprovision
|
BITWARDEN_PROFILE_PATH=$HOME/secrets/dist_bitwarden.mobileprovision
|
||||||
EXTENSION_PROFILE_PATH=$HOME/secrets/dist_extension.mobileprovision
|
EXTENSION_PROFILE_PATH=$HOME/secrets/dist_extension.mobileprovision
|
||||||
SHARE_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_share_extension.mobileprovision
|
SHARE_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_share_extension.mobileprovision
|
||||||
WATCH_APP_PROFILE_PATH=$HOME/secrets/dist_watch_app.mobileprovision
|
WATCH_APP_PROFILE_PATH=$HOME/secrets/dist_bitwarden_watch_app.mobileprovision
|
||||||
WATCH_APP_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_watch_app_extension.mobileprovision
|
WATCH_APP_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_bitwarden_watch_app_extension.mobileprovision
|
||||||
PROFILES_DIR_PATH=$HOME/Library/MobileDevice/Provisioning\ Profiles
|
PROFILES_DIR_PATH=$HOME/Library/MobileDevice/Provisioning\ Profiles
|
||||||
|
|
||||||
mkdir -p "$PROFILES_DIR_PATH"
|
mkdir -p "$PROFILES_DIR_PATH"
|
||||||
@@ -597,74 +588,50 @@ jobs:
|
|||||||
|
|
||||||
- name: Bulid WatchApp
|
- name: Bulid WatchApp
|
||||||
run: |
|
run: |
|
||||||
echo "########################################"
|
|
||||||
echo "##### Build WatchApp with Release Configuration"
|
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
|
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
|
- name: Archive Build for App Store
|
||||||
run: |
|
run: |
|
||||||
Write-Output "########################################"
|
echo "##### Archive for Release ios-arm64"
|
||||||
Write-Output "##### Archive for Release ios-arm64
|
|
||||||
Write-Output "########################################"
|
|
||||||
|
|
||||||
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
|
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
|
- name: Archive Build for Mobile Automation
|
||||||
run: |
|
run: |
|
||||||
Write-Output "########################################"
|
echo "##### Archive Debug for iossimulator-x64"
|
||||||
Write-Output "##### Archive Debug for iossimulator-x64
|
|
||||||
Write-Output "########################################"
|
|
||||||
|
|
||||||
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
|
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
|
||||||
|
ls $HOME/Library/Developer/Xcode/Archives
|
||||||
Write-Output "########################################"
|
|
||||||
Write-Output "##### Done"
|
|
||||||
Write-Output "########################################"
|
|
||||||
ls ~/Library/Developer/Xcode/Archives
|
|
||||||
shell: pwsh
|
|
||||||
|
|
||||||
- name: Export .ipa for App Store
|
- name: Export .ipa for App Store
|
||||||
|
env:
|
||||||
|
EXPORT_OPTIONS_PATH: ./.github/resources/export-options-app-store.plist
|
||||||
|
EXPORT_PATH: ./bitwarden-export
|
||||||
run: |
|
run: |
|
||||||
EXPORT_OPTIONS_PATH="./.github/resources/export-options-app-store.plist"
|
|
||||||
ARCHIVE_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive"
|
ARCHIVE_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive"
|
||||||
EXPORT_PATH="./bitwarden-export"
|
|
||||||
|
|
||||||
xcodebuild -exportArchive -archivePath $ARCHIVE_PATH -exportPath $EXPORT_PATH \
|
xcodebuild -exportArchive -archivePath $ARCHIVE_PATH -exportPath $EXPORT_PATH \
|
||||||
-exportOptionsPlist $EXPORT_OPTIONS_PATH
|
-exportOptionsPlist $EXPORT_OPTIONS_PATH
|
||||||
|
|
||||||
- name: Export .app for Automation CI
|
- 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: |
|
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
|
zip -r -q ${{ env.app_ci_output_filename }}.app.zip $ARCHIVE_PATH
|
||||||
mv ${{ env.app_ci_output_filename }}.app.zip $EXPORT_PATH
|
mv ${{ env.app_ci_output_filename }}.app.zip $EXPORT_PATH
|
||||||
|
|
||||||
- name: Copy all dSYMs files to upload
|
- 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: |
|
run: |
|
||||||
ARCHIVE_DSYMS_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive/dSYMs"
|
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
|
cp -r -v $ARCHIVE_DSYMS_PATH $EXPORT_PATH
|
||||||
mkdir $WATCH_DSYMS_EXPORT_PATH
|
mkdir $WATCH_DSYMS_EXPORT_PATH
|
||||||
cp -r -v $WATCH_ARCHIVE_DSYMS_PATH $WATCH_DSYMS_EXPORT_PATH
|
cp -r -v $WATCH_ARCHIVE_DSYMS_PATH $WATCH_DSYMS_EXPORT_PATH
|
||||||
|
|
||||||
- name: Upload App Store .ipa & dSYMs artifacts
|
- name: Upload App Store .ipa & dSYMs artifacts
|
||||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||||
with:
|
with:
|
||||||
name: Bitwarden iOS
|
name: Bitwarden iOS
|
||||||
path: |
|
path: |
|
||||||
@@ -673,7 +640,7 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload .app file for Automation CI
|
- name: Upload .app file for Automation CI
|
||||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||||
with:
|
with:
|
||||||
name: ${{ env.app_ci_output_filename }}.app.zip
|
name: ${{ env.app_ci_output_filename }}.app.zip
|
||||||
path: ./bitwarden-export/${{ 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/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
|| github.ref == 'refs/heads/hotfix-rc'
|
|| github.ref == 'refs/heads/hotfix-rc'
|
||||||
run: |
|
run: |
|
||||||
echo "########################################"
|
|
||||||
echo "##### Uploading Watch dSYMs to Firebase"
|
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" \;
|
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
|
- name: Validate app in App Store
|
||||||
@@ -726,7 +690,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
xcrun altool --validate-app --type ios --file "./bitwarden-export/Bitwarden.ipa" \
|
xcrun altool --validate-app --type ios --file "./bitwarden-export/Bitwarden.ipa" \
|
||||||
--username "$APPLE_ID_USERNAME" --password "$APPLE_ID_PASSWORD"
|
--username "$APPLE_ID_USERNAME" --password "$APPLE_ID_PASSWORD"
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Deploy to App Store
|
- name: Deploy to App Store
|
||||||
if: |
|
if: |
|
||||||
@@ -755,7 +718,7 @@ jobs:
|
|||||||
_CROWDIN_PROJECT_ID: "269690"
|
_CROWDIN_PROJECT_ID: "269690"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
|
|
||||||
- name: Login to Azure - CI Subscription
|
- name: Login to Azure - CI Subscription
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
@@ -770,13 +733,13 @@ jobs:
|
|||||||
secrets: "crowdin-api-token"
|
secrets: "crowdin-api-token"
|
||||||
|
|
||||||
- name: Upload Sources
|
- name: Upload Sources
|
||||||
uses: crowdin/github-action@198daeb2d30636c4608d6a6bb96c009dbefc02a2 # v1.18.0
|
uses: crowdin/github-action@61ac8b980551f674046220c3e104bddae2916ac5 # v2.0.0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||||
with:
|
with:
|
||||||
config: crowdin.yml
|
config: crowdin.yml
|
||||||
crowdin_branch_name: main
|
crowdin_branch_name: main
|
||||||
upload_sources: true
|
upload_sources: true
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
|
|
||||||
@@ -794,27 +757,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check if any job failed
|
- name: Check if any job failed
|
||||||
if: |
|
if: |
|
||||||
(github.ref == 'refs/heads/main')
|
(github.ref == 'refs/heads/main'
|
||||||
|| (github.ref == 'refs/heads/rc')
|
|| github.ref == 'refs/heads/rc'
|
||||||
|| (github.ref == 'refs/heads/hotfix-rc')
|
|| github.ref == 'refs/heads/hotfix-rc')
|
||||||
env:
|
&& contains(needs.*.result, 'failure')
|
||||||
CLOC_STATUS: ${{ needs.cloc.result }}
|
run: exit 1
|
||||||
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
|
|
||||||
|
|
||||||
- name: Login to Azure - CI Subscription
|
- name: Login to Azure - CI Subscription
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
@@ -831,7 +778,7 @@ jobs:
|
|||||||
secrets: "devops-alerts-slack-webhook-url"
|
secrets: "devops-alerts-slack-webhook-url"
|
||||||
|
|
||||||
- name: Notify Slack on failure
|
- name: Notify Slack on failure
|
||||||
uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0
|
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
|
||||||
if: failure()
|
if: failure()
|
||||||
env:
|
env:
|
||||||
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
||||||
|
|||||||
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@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
|
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
|
||||||
6
.github/workflows/crowdin-pull.yml
vendored
6
.github/workflows/crowdin-pull.yml
vendored
@@ -10,12 +10,12 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
crowdin-sync:
|
crowdin-sync:
|
||||||
name: Autosync
|
name: Autosync
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
env:
|
env:
|
||||||
_CROWDIN_PROJECT_ID: "269690"
|
_CROWDIN_PROJECT_ID: "269690"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
|
|
||||||
- name: Login to Azure - CI Subscription
|
- name: Login to Azure - CI Subscription
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: crowdin/github-action@198daeb2d30636c4608d6a6bb96c009dbefc02a2 # v1.18.0
|
uses: crowdin/github-action@61ac8b980551f674046220c3e104bddae2916ac5 # v2.0.0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||||
|
|||||||
2
.github/workflows/enforce-labels.yml
vendored
2
.github/workflows/enforce-labels.yml
vendored
@@ -7,7 +7,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
enforce-label:
|
enforce-label:
|
||||||
name: EnforceLabel
|
name: EnforceLabel
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Enforce Label
|
- name: Enforce Label
|
||||||
uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # 2.2.2
|
uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # 2.2.2
|
||||||
|
|||||||
4
.github/workflows/pr-labeler.yml
vendored
4
.github/workflows/pr-labeler.yml
vendored
@@ -10,8 +10,8 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 # v4.3.0
|
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
|
||||||
with:
|
with:
|
||||||
sync-labels: true
|
sync-labels: true
|
||||||
|
|||||||
111
.github/workflows/release.yml
vendored
111
.github/workflows/release.yml
vendored
@@ -23,12 +23,12 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Create Release
|
name: Create Release
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
outputs:
|
outputs:
|
||||||
branch-name: ${{ steps.branch.outputs.branch-name }}
|
branch-name: ${{ steps.branch.outputs.branch-name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Branch check
|
- name: Branch check
|
||||||
if: github.event.inputs.release_type != 'Dry Run'
|
if: inputs.release_type != 'Dry Run'
|
||||||
run: |
|
run: |
|
||||||
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
|
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
|
||||||
echo "==================================="
|
echo "==================================="
|
||||||
@@ -38,15 +38,15 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
|
|
||||||
- name: Check Release Version
|
- name: Check Release Version
|
||||||
id: version
|
id: version
|
||||||
uses: bitwarden/gh-actions/release-version-check@main
|
uses: bitwarden/gh-actions/release-version-check@main
|
||||||
with:
|
with:
|
||||||
release-type: ${{ github.event.inputs.release_type }}
|
release-type: ${{ inputs.release_type }}
|
||||||
project-type: xamarin
|
project-type: xamarin
|
||||||
file: src/Android/Properties/AndroidManifest.xml
|
file: src/App/Platforms/Android/AndroidManifest.xml
|
||||||
|
|
||||||
- name: Get branch name
|
- name: Get branch name
|
||||||
id: branch
|
id: branch
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Create GitHub deployment
|
- name: Create GitHub deployment
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||||
uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7
|
uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7
|
||||||
id: deployment
|
id: deployment
|
||||||
with:
|
with:
|
||||||
@@ -65,18 +65,17 @@ jobs:
|
|||||||
description: 'Deployment ${{ steps.version.outputs.version }} from branch ${{ steps.branch.outputs.branch-name }}'
|
description: 'Deployment ${{ steps.version.outputs.version }} from branch ${{ steps.branch.outputs.branch-name }}'
|
||||||
task: release
|
task: release
|
||||||
|
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||||
uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0
|
uses: bitwarden/gh-actions/download-artifacts@main
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: ${{ steps.branch.outputs.branch-name }}
|
branch: ${{ steps.branch.outputs.branch-name }}
|
||||||
|
|
||||||
- name: Dry Run - Download all artifacts
|
- name: Dry Run - Download all artifacts
|
||||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
if: ${{ inputs.release_type == 'Dry Run' }}
|
||||||
uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0
|
uses: bitwarden/gh-actions/download-artifacts@main
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
@@ -86,7 +85,7 @@ jobs:
|
|||||||
run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS
|
run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||||
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0
|
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0
|
||||||
with:
|
with:
|
||||||
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
|
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
|
||||||
@@ -103,16 +102,16 @@ jobs:
|
|||||||
draft: true
|
draft: true
|
||||||
|
|
||||||
- name: Update deployment status to Success
|
- name: Update deployment status to Success
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }}
|
if: ${{ inputs.release_type != 'Dry Run' && success() }}
|
||||||
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
|
||||||
with:
|
with:
|
||||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
state: 'success'
|
state: 'success'
|
||||||
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||||
|
|
||||||
- name: Update deployment status to Failure
|
- name: Update deployment status to Failure
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }}
|
if: ${{ inputs.release_type != 'Dry Run' && failure() }}
|
||||||
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
|
||||||
with:
|
with:
|
||||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
state: 'failure'
|
state: 'failure'
|
||||||
@@ -121,16 +120,16 @@ jobs:
|
|||||||
|
|
||||||
f-droid:
|
f-droid:
|
||||||
name: F-Droid Release
|
name: F-Droid Release
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
needs: release
|
needs: release
|
||||||
if: inputs.fdroid_publish
|
if: inputs.fdroid_publish
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
|
|
||||||
- name: Download F-Droid .apk artifact
|
- name: Download F-Droid .apk artifact
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||||
uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0
|
uses: bitwarden/gh-actions/download-artifacts@main
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
@@ -138,8 +137,8 @@ jobs:
|
|||||||
name: com.x8bit.bitwarden-fdroid.apk
|
name: com.x8bit.bitwarden-fdroid.apk
|
||||||
|
|
||||||
- name: Dry Run - Download F-Droid .apk artifact
|
- name: Dry Run - Download F-Droid .apk artifact
|
||||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
if: ${{ inputs.release_type == 'Dry Run' }}
|
||||||
uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0
|
uses: bitwarden/gh-actions/download-artifacts@main
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
@@ -152,9 +151,7 @@ jobs:
|
|||||||
node-version: '16.x'
|
node-version: '16.x'
|
||||||
|
|
||||||
- name: Set up F-Droid server
|
- name: Set up F-Droid server
|
||||||
run: |
|
run: pip install git+https://gitlab.com/fdroid/fdroidserver.git
|
||||||
sudo apt-get -qq update
|
|
||||||
sudo apt-get -qqy install --no-install-recommends fdroidserver wget
|
|
||||||
|
|
||||||
- name: Set up Git credentials
|
- name: Set up Git credentials
|
||||||
env:
|
env:
|
||||||
@@ -167,49 +164,59 @@ jobs:
|
|||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
node --version
|
echo "Node Version: $(node --version)"
|
||||||
npm --version
|
echo "NPM Version: $(npm --version)"
|
||||||
git --version
|
echo "Git Version: $(git --version)"
|
||||||
|
echo "F-Droid Server Version: $(fdroid --version)"
|
||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Install Node dependencies
|
- name: Install Node dependencies
|
||||||
run: npm install
|
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:
|
env:
|
||||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
ACCOUNT_NAME: bitwardenci
|
||||||
|
CONTAINER_NAME: mobile
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/secrets
|
mkdir -p $HOME/secrets
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||||
--output ./store/fdroid/keystore.jks ./.github/secrets/store_fdroid-keystore.jks.gpg
|
--name store_fdroid-keystore.jks --file ./store/fdroid/keystore.jks --output none
|
||||||
|
|
||||||
- name: Compile for F-Droid Store
|
- name: Compile for F-Droid Store
|
||||||
env:
|
env:
|
||||||
FDROID_STORE_KEYSTORE_PASSWORD: ${{ secrets.FDROID_STORE_KEYSTORE_PASSWORD }}
|
FDROID_STORE_KEYSTORE_PASSWORD: ${{ secrets.FDROID_STORE_KEYSTORE_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
cd $GITHUB_WORKSPACE
|
# Create required directories.
|
||||||
mkdir dist
|
mkdir dist
|
||||||
cp CNAME ./dist
|
mkdir -p store/temp/fdroid
|
||||||
cd store
|
mkdir -p store/fdroid/repo
|
||||||
chmod 600 fdroid/config.py fdroid/keystore.jks
|
|
||||||
mkdir -p temp/fdroid
|
# Configure F-Droid server.
|
||||||
|
cp CNAME dist/
|
||||||
|
chmod 600 store/fdroid/config.yml store/fdroid/keystore.jks
|
||||||
TEMP_DIR="$GITHUB_WORKSPACE/store/temp/fdroid"
|
TEMP_DIR="$GITHUB_WORKSPACE/store/temp/fdroid"
|
||||||
cd fdroid
|
echo "keypass: $FDROID_STORE_KEYSTORE_PASSWORD" >> store/fdroid/config.yml
|
||||||
echo "keypass=\"$FDROID_STORE_KEYSTORE_PASSWORD\"" >>config.py
|
echo "keystorepass: $FDROID_STORE_KEYSTORE_PASSWORD" >> store/fdroid/config.yml
|
||||||
echo "keystorepass=\"$FDROID_STORE_KEYSTORE_PASSWORD\"" >>config.py
|
echo "local_copy_dir: $TEMP_DIR" >> store/fdroid/config.yml
|
||||||
echo "local_copy_dir=\"$TEMP_DIR\"" >>config.py
|
mv $GITHUB_WORKSPACE/com.x8bit.bitwarden-fdroid.apk store/fdroid/repo/
|
||||||
mkdir -p repo
|
|
||||||
mv $GITHUB_WORKSPACE/com.x8bit.bitwarden-fdroid.apk ./repo/
|
# Run update and deploy.
|
||||||
|
cd store/fdroid
|
||||||
fdroid update
|
fdroid update
|
||||||
fdroid server update
|
fdroid deploy
|
||||||
cd ..
|
cd ../..
|
||||||
rm -rf temp/fdroid/archive
|
|
||||||
mv -v temp/fdroid ../dist
|
# Move files for distribution.
|
||||||
cd fdroid
|
rm -rf store/temp/fdroid/archive
|
||||||
cp index.html btn.png qr.png ../../dist/fdroid
|
mv -v store/temp/fdroid dist
|
||||||
cd $GITHUB_WORKSPACE
|
cp store/fdroid/index.html store/fdroid/btn.png store/fdroid/qr.png dist/fdroid
|
||||||
|
|
||||||
- name: Deploy to gh-pages
|
- name: Deploy to gh-pages
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||||
run: npm run deploy
|
run: npm run deploy
|
||||||
|
|||||||
2
.github/workflows/stale-bot.yml
vendored
2
.github/workflows/stale-bot.yml
vendored
@@ -8,7 +8,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
name: 'Check for stale issues and PRs'
|
name: 'Check for stale issues and PRs'
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: 'Run stale action'
|
- name: 'Run stale action'
|
||||||
uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
||||||
|
|||||||
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
|
name: Bump Mobile Version
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
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
|
- name: Login to Azure - CI Subscription
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
with:
|
with:
|
||||||
@@ -41,9 +23,9 @@ jobs:
|
|||||||
keyvault: bitwarden-ci
|
keyvault: bitwarden-ci
|
||||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
- name: "Bump version to ${{ steps.version.outputs.new_version }}"
|
- name: Trigger Version Bump workflow
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
GH_TOKEN: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
run: |
|
run: |
|
||||||
echo '{"cut_rc_branch": "false", "version_number": "${{ steps.version.outputs.new_version }}"}' | \
|
echo '{"cut_rc_branch": "false"}' | \
|
||||||
gh workflow run version-bump.yml --json --repo bitwarden/mobile
|
gh workflow run version-bump.yml --json --repo bitwarden/mobile
|
||||||
|
|||||||
190
.github/workflows/version-bump.yml
vendored
190
.github/workflows/version-bump.yml
vendored
@@ -1,39 +1,45 @@
|
|||||||
---
|
---
|
||||||
name: Version Bump
|
name: Version Bump
|
||||||
run-name: Version Bump - v${{ inputs.version_number }}
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version_number:
|
version_number_override:
|
||||||
description: "New version (example: '2024.1.0')"
|
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
|
||||||
required: true
|
required: false
|
||||||
|
type: string
|
||||||
cut_rc_branch:
|
cut_rc_branch:
|
||||||
description: "Cut RC branch?"
|
description: "Cut RC branch?"
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
|
enable_slack_notification:
|
||||||
|
description: "Enable Slack notifications for upcoming release?"
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bump_version:
|
bump_version:
|
||||||
name: "Bump Version to v${{ inputs.version_number }}"
|
name: Bump Version
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.set-final-version-output.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Azure - CI Subscription
|
- name: Validate version input
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
if: ${{ inputs.version_number_override != '' }}
|
||||||
|
uses: bitwarden/gh-actions/version-check@main
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
version: ${{ inputs.version_number_override }}
|
||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Slack Notification Check
|
||||||
id: retrieve-secrets
|
run: |
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
if [[ "${{ inputs.enable_slack_notification }}" == true ]]; then
|
||||||
with:
|
echo "Slack notifications enabled."
|
||||||
keyvault: "bitwarden-ci"
|
else
|
||||||
secrets: "github-gpg-private-key,
|
echo "Slack notifications disabled."
|
||||||
github-gpg-private-key-passphrase,
|
fi
|
||||||
github-pat-bitwarden-devops-bot-repo-scope"
|
|
||||||
|
|
||||||
- name: Checkout Branch
|
- name: Checkout Branch
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
@@ -47,6 +53,20 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
- name: Import GPG key
|
||||||
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
|
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
|
||||||
with:
|
with:
|
||||||
@@ -55,25 +75,38 @@ jobs:
|
|||||||
git_user_signingkey: true
|
git_user_signingkey: true
|
||||||
git_commit_gpgsign: 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
|
- name: Create Version Branch
|
||||||
id: create-branch
|
id: create-branch
|
||||||
run: |
|
run: |
|
||||||
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }}
|
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
|
||||||
git switch -c $NAME
|
git switch -c $NAME
|
||||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
echo "name=$NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Install xmllint
|
- 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
|
- name: Get current version
|
||||||
env:
|
id: current-version
|
||||||
NEW_VERSION: ${{ inputs.version_number }}
|
|
||||||
run: |
|
run: |
|
||||||
CURRENT_VERSION=$(xmllint --xpath '
|
CURRENT_VERSION=$(xmllint --xpath '
|
||||||
string(/manifest/@*[local-name()="versionName"
|
string(/manifest/@*[local-name()="versionName"
|
||||||
and namespace-uri()="http://schemas.android.com/apk/res/android"])
|
and namespace-uri()="http://schemas.android.com/apk/res/android"])
|
||||||
' src/App/Platforms/Android/AndroidManifest.xml)
|
' 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.
|
# Error if version has not changed.
|
||||||
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
||||||
echo "Version has not changed."
|
echo "Version has not changed."
|
||||||
@@ -89,40 +122,93 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
with:
|
with:
|
||||||
version: ${{ inputs.version_number }}
|
|
||||||
file_path: "src/App/Platforms/Android/AndroidManifest.xml"
|
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
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
with:
|
with:
|
||||||
version: ${{ inputs.version_number }}
|
|
||||||
file_path: "src/iOS.Autofill/Info.plist"
|
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
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
with:
|
with:
|
||||||
version: ${{ inputs.version_number }}
|
|
||||||
file_path: "src/iOS.Extension/Info.plist"
|
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
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
with:
|
with:
|
||||||
version: ${{ inputs.version_number }}
|
|
||||||
file_path: "src/iOS.ShareExtension/Info.plist"
|
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
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
with:
|
with:
|
||||||
version: ${{ inputs.version_number }}
|
file_path: "src/iOS.ShareExtension/Info.plist"
|
||||||
file_path: "src/App/Platforms/iOS/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: |
|
run: |
|
||||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
if [[ "${{ steps.bump-version-override.outcome }}" == "success" ]]; then
|
||||||
git config --local user.name "bitwarden-devops-bot"
|
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
|
- name: Check if version changed
|
||||||
id: version-changed
|
id: version-changed
|
||||||
@@ -136,7 +222,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Commit files
|
- name: Commit files
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
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
|
- name: Push changes
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
@@ -150,7 +236,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
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: |
|
run: |
|
||||||
PR_URL=$(gh pr create --title "$TITLE" \
|
PR_URL=$(gh pr create --title "$TITLE" \
|
||||||
--base "main" \
|
--base "main" \
|
||||||
@@ -166,45 +252,57 @@ jobs:
|
|||||||
- [X] Other
|
- [X] Other
|
||||||
|
|
||||||
## Objective
|
## 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
|
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Approve PR
|
- name: Approve PR
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||||
run: gh pr review $PR_NUMBER --approve
|
run: gh pr review $PR_NUMBER --approve
|
||||||
|
|
||||||
- name: Merge PR
|
- name: Merge PR
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||||
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
||||||
|
|
||||||
|
- name: Report upcoming release version to Slack
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && inputs.enable_slack_notification == true }}
|
||||||
|
uses: bitwarden/gh-actions/report-upcoming-release-version@main
|
||||||
|
with:
|
||||||
|
version: ${{ steps.set-final-version-output.outputs.version }}
|
||||||
|
project: ${{ github.repository }}
|
||||||
|
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
cut_rc:
|
cut_rc:
|
||||||
name: Cut RC branch
|
name: Cut RC branch
|
||||||
needs: bump_version
|
|
||||||
if: ${{ inputs.cut_rc_branch == true }}
|
if: ${{ inputs.cut_rc_branch == true }}
|
||||||
|
needs: bump_version
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Branch
|
- name: Checkout Branch
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Install xmllint
|
- name: Install xmllint
|
||||||
run: sudo apt install -y libxml2-utils
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libxml2-utils
|
||||||
|
|
||||||
- name: Verify version has been updated
|
- name: Verify version has been updated
|
||||||
env:
|
env:
|
||||||
NEW_VERSION: ${{ inputs.version_number }}
|
NEW_VERSION: ${{ needs.bump_version.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
# Wait for version to change.
|
# Wait for version to change.
|
||||||
while : ; do
|
while : ; do
|
||||||
echo "Waiting for version to be updated..."
|
echo "Waiting for version to be updated..."
|
||||||
git pull --force
|
git pull --force
|
||||||
CURRENT_VERSION=$(xmllint --xpath '
|
CURRENT_VERSION=$(xmllint --xpath '
|
||||||
string(/manifest/@*[local-name()="versionName"
|
string(/manifest/@*[local-name()="versionName"
|
||||||
and namespace-uri()="http://schemas.android.com/apk/res/android"])
|
and namespace-uri()="http://schemas.android.com/apk/res/android"])
|
||||||
' src/App/Platforms/Android/AndroidManifest.xml)
|
' src/App/Platforms/Android/AndroidManifest.xml)
|
||||||
|
|
||||||
|
|||||||
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
|
# NuGet Packages
|
||||||
*.nupkg
|
*.nupkg
|
||||||
|
!**/Xamarin.AndroidX.Credentials.1.0.0.nupkg
|
||||||
# The packages folder can be ignored because of Package Restore
|
# The packages folder can be ignored because of Package Restore
|
||||||
**/packages/*
|
**/packages/*
|
||||||
# except build/, which is used as an MSBuild target.
|
# except build/, which is used as an MSBuild target.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<MauiVersion>8.0.4-nightly.*</MauiVersion>
|
<MauiVersion>8.0.7</MauiVersion>
|
||||||
<ReleaseCodesignProvision>Automatic:AppStore</ReleaseCodesignProvision>
|
<ReleaseCodesignProvision>Automatic:AppStore</ReleaseCodesignProvision>
|
||||||
<ReleaseCodesignKey>iPhone Distribution</ReleaseCodesignKey>
|
<ReleaseCodesignKey>iPhone Distribution</ReleaseCodesignKey>
|
||||||
<IncludeBitwardeniOSExtensions>True</IncludeBitwardeniOSExtensions>
|
<IncludeBitwardeniOSExtensions>True</IncludeBitwardeniOSExtensions>
|
||||||
@@ -9,5 +9,8 @@
|
|||||||
|
|
||||||
<!-- Uncomment this when Unit Testing-->
|
<!-- Uncomment this when Unit Testing-->
|
||||||
<!-- <CustomConstants>UT</CustomConstants> -->
|
<!-- <CustomConstants>UT</CustomConstants> -->
|
||||||
|
|
||||||
|
<!-- Uncomment this when building FDROID-->
|
||||||
|
<!-- <CustomConstants>FDROID</CustomConstants> -->
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ The Bitwarden mobile application is written in C# using .NET MAUI.
|
|||||||
|
|
||||||
# Build/Run
|
# Build/Run
|
||||||
|
|
||||||
Please refer to the [Mobile section](https://contributing.bitwarden.com/getting-started/clients/mobile/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.
|
Please refer to the [Mobile section](https://contributing.bitwarden.com/getting-started/mobile/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.
|
||||||
|
|
||||||
# We're Hiring!
|
# We're Hiring!
|
||||||
|
|
||||||
|
|||||||
59
build.cake
59
build.cake
@@ -15,16 +15,18 @@ abstract record VariantConfig(
|
|||||||
string AppName,
|
string AppName,
|
||||||
string AndroidPackageName,
|
string AndroidPackageName,
|
||||||
string iOSBundleId,
|
string iOSBundleId,
|
||||||
string ApsEnvironment
|
string ApsEnvironment,
|
||||||
|
string DistProvisioningProfilePrefix
|
||||||
);
|
);
|
||||||
|
|
||||||
const string BASE_BUNDLE_ID_DROID = "com.x8bit.bitwarden";
|
const string BASE_BUNDLE_ID_DROID = "com.x8bit.bitwarden";
|
||||||
const string BASE_BUNDLE_ID_IOS = "com.8bit.bitwarden";
|
const string BASE_BUNDLE_ID_IOS = "com.8bit.bitwarden";
|
||||||
|
|
||||||
record Dev(): VariantConfig("Bitwarden Dev", $"{BASE_BUNDLE_ID_DROID}.dev", $"{BASE_BUNDLE_ID_IOS}.dev", "development");
|
//NOTE: Beta iOS variants have a different ITSEncryptionExportComplianceCode
|
||||||
record QA(): VariantConfig("Bitwarden QA", $"{BASE_BUNDLE_ID_DROID}.qa", $"{BASE_BUNDLE_ID_IOS}.qa", "development");
|
record Dev(): VariantConfig("Bitwarden Dev", $"{BASE_BUNDLE_ID_DROID}.dev", $"{BASE_BUNDLE_ID_IOS}.dev", "development", "Dist:");
|
||||||
record Beta(): VariantConfig("Bitwarden Beta", $"{BASE_BUNDLE_ID_DROID}.beta", $"{BASE_BUNDLE_ID_IOS}.beta", "production");
|
record QA(): VariantConfig("Bitwarden QA", $"{BASE_BUNDLE_ID_DROID}.qa", $"{BASE_BUNDLE_ID_IOS}.qa", "development", "Dist:");
|
||||||
record Prod(): VariantConfig("Bitwarden", $"{BASE_BUNDLE_ID_DROID}", $"{BASE_BUNDLE_ID_IOS}", "production");
|
record Beta(): VariantConfig("Bitwarden Beta", $"{BASE_BUNDLE_ID_DROID}.beta", $"{BASE_BUNDLE_ID_IOS}.beta", "production", "Dist: Beta");
|
||||||
|
record Prod(): VariantConfig("Bitwarden", $"{BASE_BUNDLE_ID_DROID}", $"{BASE_BUNDLE_ID_IOS}", "production", "Dist:");
|
||||||
|
|
||||||
VariantConfig GetVariant() => variant.ToLower() switch{
|
VariantConfig GetVariant() => variant.ToLower() switch{
|
||||||
"qa" => new QA(),
|
"qa" => new QA(),
|
||||||
@@ -197,7 +199,8 @@ private void UpdateiOSInfoPlist(string plistPath, VariantConfig buildVariant, Gi
|
|||||||
var prevBundleId = plist["CFBundleIdentifier"];
|
var prevBundleId = plist["CFBundleIdentifier"];
|
||||||
var prevBundleName = plist["CFBundleName"];
|
var prevBundleName = plist["CFBundleName"];
|
||||||
//var newVersion = CreateBuildNumber(prevVersion).ToString();
|
//var newVersion = CreateBuildNumber(prevVersion).ToString();
|
||||||
var newVersionName = GetVersionName(prevVersionName, buildVariant, git);
|
// we need to maintain version formatting here composed of one to three period-separated integers, so we cannot use the GetVersionName method as in Android for non-Prod.
|
||||||
|
var newVersionName = prevVersionName;
|
||||||
var newBundleId = GetiOSBundleId(buildVariant, projectType);
|
var newBundleId = GetiOSBundleId(buildVariant, projectType);
|
||||||
var newBundleName = GetiOSBundleName(buildVariant, projectType);
|
var newBundleName = GetiOSBundleName(buildVariant, projectType);
|
||||||
|
|
||||||
@@ -219,6 +222,11 @@ private void UpdateiOSInfoPlist(string plistPath, VariantConfig buildVariant, Gi
|
|||||||
plist["NSExtension"]["NSExtensionAttributes"]["NSExtensionActivationRule"] = keyText.Replace("com.8bit.bitwarden", buildVariant.iOSBundleId);
|
plist["NSExtension"]["NSExtensionAttributes"]["NSExtensionActivationRule"] = keyText.Replace("com.8bit.bitwarden", buildVariant.iOSBundleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(buildVariant is Beta)
|
||||||
|
{
|
||||||
|
plist["ITSEncryptionExportComplianceCode"] = "3dd3e32f-efa6-4d99-b410-28aa28b1cb77";
|
||||||
|
}
|
||||||
|
|
||||||
SerializePlist(plistFile, plist);
|
SerializePlist(plistFile, plist);
|
||||||
|
|
||||||
Information($"Changed app name from {prevBundleName} to {newBundleName}");
|
Information($"Changed app name from {prevBundleName} to {newBundleName}");
|
||||||
@@ -228,12 +236,15 @@ private void UpdateiOSInfoPlist(string plistPath, VariantConfig buildVariant, Gi
|
|||||||
Information($"{plistPath} updated with success!");
|
Information($"{plistPath} updated with success!");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateiOSEntitlementsPlist(string entitlementsPath, VariantConfig buildVariant)
|
private void UpdateiOSEntitlementsPlist(string entitlementsPath, VariantConfig buildVariant, bool updateApsEnv)
|
||||||
{
|
{
|
||||||
var EntitlementlistFile = File(entitlementsPath);
|
var EntitlementlistFile = File(entitlementsPath);
|
||||||
dynamic Entitlements = DeserializePlist(EntitlementlistFile);
|
dynamic Entitlements = DeserializePlist(EntitlementlistFile);
|
||||||
|
|
||||||
Entitlements["aps-environment"] = buildVariant.ApsEnvironment;
|
if (updateApsEnv)
|
||||||
|
{
|
||||||
|
Entitlements["aps-environment"] = buildVariant.ApsEnvironment;
|
||||||
|
}
|
||||||
Entitlements["keychain-access-groups"] = new List<string>() { "$(AppIdentifierPrefix)" + buildVariant.iOSBundleId };
|
Entitlements["keychain-access-groups"] = new List<string>() { "$(AppIdentifierPrefix)" + buildVariant.iOSBundleId };
|
||||||
Entitlements["com.apple.security.application-groups"] = new List<string>() { $"group.{buildVariant.iOSBundleId}" };;
|
Entitlements["com.apple.security.application-groups"] = new List<string>() { $"group.{buildVariant.iOSBundleId}" };;
|
||||||
|
|
||||||
@@ -272,9 +283,10 @@ private void UpdateWatchPbxproj(string pbxprojPath, string newVersion)
|
|||||||
const string pattern = @"MARKETING_VERSION = [^;]*;";
|
const string pattern = @"MARKETING_VERSION = [^;]*;";
|
||||||
|
|
||||||
fileText = Regex.Replace(fileText, pattern, $"MARKETING_VERSION = {newVersion};");
|
fileText = Regex.Replace(fileText, pattern, $"MARKETING_VERSION = {newVersion};");
|
||||||
|
|
||||||
FileWriteText(pbxprojPath, fileText);
|
FileWriteText(pbxprojPath, fileText);
|
||||||
Information($"{pbxprojPath} modified successfully.");
|
|
||||||
|
Information($"{pbxprojPath} modified Marketing Version successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -327,7 +339,7 @@ Task("UpdateiOSPlist")
|
|||||||
var infoPath = Path.Combine(_slnPath, "src", "App", "Platforms", "iOS", "Info.plist");
|
var infoPath = Path.Combine(_slnPath, "src", "App", "Platforms", "iOS", "Info.plist");
|
||||||
var entitlementsPath = Path.Combine(_slnPath, "src", "App", "Platforms", "iOS", "Entitlements.plist");
|
var entitlementsPath = Path.Combine(_slnPath, "src", "App", "Platforms", "iOS", "Entitlements.plist");
|
||||||
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.MainApp);
|
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.MainApp);
|
||||||
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant);
|
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
Task("UpdateiOSAutofillPlist")
|
Task("UpdateiOSAutofillPlist")
|
||||||
@@ -338,7 +350,7 @@ Task("UpdateiOSAutofillPlist")
|
|||||||
var infoPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Info.plist");
|
var infoPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Info.plist");
|
||||||
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Entitlements.plist");
|
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Entitlements.plist");
|
||||||
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Autofill);
|
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Autofill);
|
||||||
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant);
|
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
Task("UpdateiOSExtensionPlist")
|
Task("UpdateiOSExtensionPlist")
|
||||||
@@ -349,7 +361,7 @@ Task("UpdateiOSExtensionPlist")
|
|||||||
var infoPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Info.plist");
|
var infoPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Info.plist");
|
||||||
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Entitlements.plist");
|
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Entitlements.plist");
|
||||||
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Extension);
|
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Extension);
|
||||||
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant);
|
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
Task("UpdateiOSShareExtensionPlist")
|
Task("UpdateiOSShareExtensionPlist")
|
||||||
@@ -360,7 +372,7 @@ Task("UpdateiOSShareExtensionPlist")
|
|||||||
var infoPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Info.plist");
|
var infoPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Info.plist");
|
||||||
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Entitlements.plist");
|
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Entitlements.plist");
|
||||||
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.ShareExtension);
|
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.ShareExtension);
|
||||||
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant);
|
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
Task("UpdateiOSCodeFiles")
|
Task("UpdateiOSCodeFiles")
|
||||||
@@ -397,6 +409,22 @@ Task("UpdateWatchKitAppInfoPlist")
|
|||||||
UpdateWatchKitAppInfoPlist(infoPath, buildVariant);
|
UpdateWatchKitAppInfoPlist(infoPath, buildVariant);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Task("UpdateDistProfiles")
|
||||||
|
.IsDependentOn("UpdateiOSCodeFiles")
|
||||||
|
.Does(()=> {
|
||||||
|
var buildVariant = GetVariant();
|
||||||
|
|
||||||
|
var filesToReplace = new string[] {
|
||||||
|
Path.Combine(".github", "resources", "export-options-app-store.plist"),
|
||||||
|
Path.Combine(_slnPath, "src", "watchOS", "bitwarden", "bitwarden.xcodeproj", "project.pbxproj")
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach(string path in filesToReplace)
|
||||||
|
{
|
||||||
|
ReplaceInFile(path, "Dist:", buildVariant.DistProvisioningProfilePrefix);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
#endregion iOS
|
#endregion iOS
|
||||||
|
|
||||||
#region Main Tasks
|
#region Main Tasks
|
||||||
@@ -418,6 +446,7 @@ Task("iOS")
|
|||||||
.IsDependentOn("UpdateiOSCodeFiles")
|
.IsDependentOn("UpdateiOSCodeFiles")
|
||||||
.IsDependentOn("UpdateWatchProject")
|
.IsDependentOn("UpdateWatchProject")
|
||||||
.IsDependentOn("UpdateWatchKitAppInfoPlist")
|
.IsDependentOn("UpdateWatchKitAppInfoPlist")
|
||||||
|
.IsDependentOn("UpdateDistProfiles")
|
||||||
.Does(()=>
|
.Does(()=>
|
||||||
{
|
{
|
||||||
Information("iOS app updated");
|
Information("iOS app updated");
|
||||||
@@ -437,4 +466,4 @@ Options:
|
|||||||
});
|
});
|
||||||
#endregion Main Tasks
|
#endregion Main Tasks
|
||||||
|
|
||||||
RunTarget(target);
|
RunTarget(target);
|
||||||
|
|||||||
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>
|
<configuration>
|
||||||
<packageSources>
|
<packageSources>
|
||||||
<add key="MAUI Nightly builds" value="https://pkgs.dev.azure.com/xamarin/public/_packaging/maui-nightly/nuget/v3/index.json" />
|
<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>
|
</packageSources>
|
||||||
</configuration>
|
</configuration>
|
||||||
@@ -117,10 +117,13 @@
|
|||||||
<Folder Include="Platforms\Android\Services\" />
|
<Folder Include="Platforms\Android\Services\" />
|
||||||
<Folder Include="Platforms\Android\Tiles\" />
|
<Folder Include="Platforms\Android\Tiles\" />
|
||||||
<Folder Include="Platforms\Android\Utilities\" />
|
<Folder Include="Platforms\Android\Utilities\" />
|
||||||
|
<Folder Include="Platforms\Android\Resources\drawable-xxxhdpi\" />
|
||||||
|
<Folder Include="Resources\Raw\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
||||||
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.18" />
|
<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.Activity.Ktx" Version="1.7.2.1" />
|
||||||
|
<PackageReference Include="Xamarin.AndroidX.Credentials" Version="1.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' AND !$(DefineConstants.Contains(FDROID))">
|
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' AND !$(DefineConstants.Contains(FDROID))">
|
||||||
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet" Version="118.0.1.5" />
|
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet" Version="118.0.1.5" />
|
||||||
@@ -256,5 +259,13 @@
|
|||||||
<None Remove="Platforms\iOS\Resources\more_vert.png" />
|
<None Remove="Platforms\iOS\Resources\more_vert.png" />
|
||||||
<None Remove="Platforms\iOS\Resources\logo_white.png" />
|
<None Remove="Platforms\iOS\Resources\logo_white.png" />
|
||||||
<None Remove="Platforms\iOS\Resources\logo%402x.png" />
|
<None Remove="Platforms\iOS\Resources\logo%402x.png" />
|
||||||
|
<None Remove="Platforms\Android\Resources\drawable-xxxhdpi\" />
|
||||||
|
<None Remove="Resources\Raw\" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">
|
||||||
|
<BundleResource Include="Platforms\iOS\PrivacyInfo.xcprivacy" LogicalName="PrivacyInfo.xcprivacy" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
||||||
|
<MauiAsset Include="Resources\Raw\fido2_privileged_allow_list.json" LogicalName="fido2_privileged_allow_list.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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.2" 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.6.0" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
||||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
|
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.NFC" />
|
<uses-permission android:name="android.permission.NFC" />
|
||||||
@@ -43,6 +43,9 @@
|
|||||||
<!-- Support for Xamarin.Essentials.Browser.OpenAsync (for Android > 11) -->
|
<!-- Support for Xamarin.Essentials.Browser.OpenAsync (for Android > 11) -->
|
||||||
<!-- Related docs: https://learn.microsoft.com/en-us/xamarin/essentials/open-browser?tabs=android -->
|
<!-- Related docs: https://learn.microsoft.com/en-us/xamarin/essentials/open-browser?tabs=android -->
|
||||||
<queries>
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.support.customtabs.action.CustomTabsService" />
|
||||||
|
</intent>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<data android:scheme="http" />
|
<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
|
// InlinePresentation requires nonNull pending intent (even though we only utilize one for the
|
||||||
// "my vault" presentation) so we're including an empty one here
|
// "my vault" presentation) so we're including an empty one here
|
||||||
pendingIntent = PendingIntent.GetService(context, 0, new Intent(),
|
pendingIntent = PendingIntent.GetService(context, 0, new Intent(),
|
||||||
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent, true));
|
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent, false));
|
||||||
}
|
}
|
||||||
var slice = CreateInlinePresentationSlice(
|
var slice = CreateInlinePresentationSlice(
|
||||||
inlinePresentationSpec,
|
inlinePresentationSpec,
|
||||||
|
|||||||
321
src/App/Platforms/Android/Autofill/CredentialHelpers.cs
Normal file
321
src/App/Platforms/Android/Autofill/CredentialHelpers.cs
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Android.App;
|
||||||
|
using Android.Content;
|
||||||
|
using Android.OS;
|
||||||
|
using AndroidX.Credentials;
|
||||||
|
using AndroidX.Credentials.Exceptions;
|
||||||
|
using AndroidX.Credentials.Provider;
|
||||||
|
using AndroidX.Credentials.WebAuthn;
|
||||||
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.App.Droid.Utilities;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Resources.Localization;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
using Bit.Core.Utilities.Fido2.Extensions;
|
||||||
|
using Bit.Droid;
|
||||||
|
using Org.Json;
|
||||||
|
using Activity = Android.App.Activity;
|
||||||
|
using Drawables = Android.Graphics.Drawables;
|
||||||
|
|
||||||
|
namespace Bit.App.Platforms.Android.Autofill
|
||||||
|
{
|
||||||
|
public static class CredentialHelpers
|
||||||
|
{
|
||||||
|
public static async Task<List<CredentialEntry>> PopulatePasskeyDataAsync(CallingAppInfo callingAppInfo,
|
||||||
|
BeginGetPublicKeyCredentialOption option, Context context, bool hasVaultBeenUnlockedInThisTransaction)
|
||||||
|
{
|
||||||
|
var passkeyEntries = new List<CredentialEntry>();
|
||||||
|
var requestOptions = new PublicKeyCredentialRequestOptions(option.RequestJson);
|
||||||
|
|
||||||
|
var authenticator = Bit.Core.Utilities.ServiceContainer.Resolve<IFido2AuthenticatorService>();
|
||||||
|
var credentials = await authenticator.SilentCredentialDiscoveryAsync(requestOptions.RpId);
|
||||||
|
|
||||||
|
// We need to change the request code for every pending intent on mapping the credential so the extras are not overriten by the last
|
||||||
|
// credential entry created.
|
||||||
|
int requestCodeAddition = 0;
|
||||||
|
passkeyEntries = credentials.Select(credential => MapCredential(credential, option, context, hasVaultBeenUnlockedInThisTransaction, Bit.Droid.Autofill.CredentialProviderService.UniqueGetRequestCode + requestCodeAddition++) as CredentialEntry).ToList();
|
||||||
|
|
||||||
|
return passkeyEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PublicKeyCredentialEntry MapCredential(Fido2AuthenticatorDiscoverableCredentialMetadata credential, BeginGetPublicKeyCredentialOption option, Context context, bool hasVaultBeenUnlockedInThisTransaction, int requestCode)
|
||||||
|
{
|
||||||
|
var credDataBundle = new Bundle();
|
||||||
|
credDataBundle.PutByteArray(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialIdIntentExtra, credential.Id);
|
||||||
|
|
||||||
|
var intent = new Intent(context, typeof(Bit.Droid.Autofill.CredentialProviderSelectionActivity))
|
||||||
|
.SetAction(Bit.Droid.Autofill.CredentialProviderService.GetFido2IntentAction).SetPackage(Constants.PACKAGE_NAME);
|
||||||
|
intent.PutExtra(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialDataIntentExtra, credDataBundle);
|
||||||
|
intent.PutExtra(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialProviderCipherId, credential.CipherId);
|
||||||
|
intent.PutExtra(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialHasVaultBeenUnlockedInThisTransactionExtra, hasVaultBeenUnlockedInThisTransaction);
|
||||||
|
var pendingIntent = PendingIntent.GetActivity(context, requestCode, intent,
|
||||||
|
PendingIntentFlags.Mutable | PendingIntentFlags.UpdateCurrent);
|
||||||
|
|
||||||
|
return new PublicKeyCredentialEntry.Builder(
|
||||||
|
context,
|
||||||
|
credential.UserName ?? "No username",
|
||||||
|
pendingIntent,
|
||||||
|
option)
|
||||||
|
.SetDisplayName(credential.UserName ?? "No username")
|
||||||
|
.SetIcon(Drawables.Icon.CreateWithResource(context, Microsoft.Maui.Resource.Drawable.icon))
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PublicKeyCredentialCreationOptions GetPublicKeyCredentialCreationOptionsFromJson(string json)
|
||||||
|
{
|
||||||
|
var request = new PublicKeyCredentialCreationOptions(json);
|
||||||
|
var jsonObj = new JSONObject(json);
|
||||||
|
var authenticatorSelection = jsonObj.GetJSONObject("authenticatorSelection");
|
||||||
|
request.AuthenticatorSelection = new AndroidX.Credentials.WebAuthn.AuthenticatorSelectionCriteria(
|
||||||
|
authenticatorSelection.OptString("authenticatorAttachment", "platform"),
|
||||||
|
authenticatorSelection.OptString("residentKey", null),
|
||||||
|
authenticatorSelection.OptBoolean("requireResidentKey", false),
|
||||||
|
authenticatorSelection.OptString("userVerification", "preferred"));
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task CreateCipherPasskeyAsync(ProviderCreateCredentialRequest getRequest, Activity activity)
|
||||||
|
{
|
||||||
|
var callingRequest = getRequest?.CallingRequest as CreatePublicKeyCredentialRequest;
|
||||||
|
|
||||||
|
if (callingRequest is null)
|
||||||
|
{
|
||||||
|
await DisplayAlertAsync(AppResources.AnErrorHasOccurred, string.Empty);
|
||||||
|
FailAndFinish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var credentialCreationOptions = GetPublicKeyCredentialCreationOptionsFromJson(callingRequest.RequestJson);
|
||||||
|
string origin;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
origin = await ValidateCallingAppInfoAndGetOriginAsync(getRequest.CallingAppInfo, credentialCreationOptions.Rp.Id);
|
||||||
|
}
|
||||||
|
catch (Core.Exceptions.ValidationException valEx)
|
||||||
|
{
|
||||||
|
await DisplayAlertAsync(AppResources.AnErrorHasOccurred, valEx.Message);
|
||||||
|
FailAndFinish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (origin is null)
|
||||||
|
{
|
||||||
|
await DisplayAlertAsync(AppResources.ErrorCreatingPasskey, AppResources.PasskeysNotSupportedForThisApp);
|
||||||
|
FailAndFinish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rp = new Core.Utilities.Fido2.PublicKeyCredentialRpEntity()
|
||||||
|
{
|
||||||
|
Id = credentialCreationOptions.Rp.Id,
|
||||||
|
Name = credentialCreationOptions.Rp.Name
|
||||||
|
};
|
||||||
|
|
||||||
|
var user = new Core.Utilities.Fido2.PublicKeyCredentialUserEntity()
|
||||||
|
{
|
||||||
|
Id = credentialCreationOptions.User.GetId(),
|
||||||
|
Name = credentialCreationOptions.User.Name,
|
||||||
|
DisplayName = credentialCreationOptions.User.DisplayName
|
||||||
|
};
|
||||||
|
|
||||||
|
var pubKeyCredParams = new List<Core.Utilities.Fido2.PublicKeyCredentialParameters>();
|
||||||
|
foreach (var pubKeyCredParam in credentialCreationOptions.PubKeyCredParams)
|
||||||
|
{
|
||||||
|
pubKeyCredParams.Add(new Core.Utilities.Fido2.PublicKeyCredentialParameters() { Alg = Convert.ToInt32(pubKeyCredParam.Alg), Type = pubKeyCredParam.Type });
|
||||||
|
}
|
||||||
|
|
||||||
|
var excludeCredentials = new List<Core.Utilities.Fido2.PublicKeyCredentialDescriptor>();
|
||||||
|
foreach (var excludeCred in credentialCreationOptions.ExcludeCredentials)
|
||||||
|
{
|
||||||
|
excludeCredentials.Add(new Core.Utilities.Fido2.PublicKeyCredentialDescriptor() { Id = excludeCred.GetId(), Type = excludeCred.Type, Transports = excludeCred.Transports.ToArray() });
|
||||||
|
}
|
||||||
|
|
||||||
|
var authenticatorSelection = new Core.Utilities.Fido2.AuthenticatorSelectionCriteria()
|
||||||
|
{
|
||||||
|
UserVerification = credentialCreationOptions.AuthenticatorSelection.UserVerification,
|
||||||
|
ResidentKey = credentialCreationOptions.AuthenticatorSelection.ResidentKey,
|
||||||
|
RequireResidentKey = credentialCreationOptions.AuthenticatorSelection.RequireResidentKey
|
||||||
|
};
|
||||||
|
|
||||||
|
var timeout = Convert.ToInt32(credentialCreationOptions.Timeout);
|
||||||
|
|
||||||
|
var credentialCreateParams = new Fido2ClientCreateCredentialParams()
|
||||||
|
{
|
||||||
|
Challenge = credentialCreationOptions.GetChallenge(),
|
||||||
|
Origin = origin,
|
||||||
|
PubKeyCredParams = pubKeyCredParams.ToArray(),
|
||||||
|
Rp = rp,
|
||||||
|
User = user,
|
||||||
|
Timeout = timeout,
|
||||||
|
Attestation = credentialCreationOptions.Attestation,
|
||||||
|
AuthenticatorSelection = authenticatorSelection,
|
||||||
|
ExcludeCredentials = excludeCredentials.ToArray(),
|
||||||
|
Extensions = MapExtensionsFromJson(credentialCreationOptions),
|
||||||
|
SameOriginWithAncestors = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var credentialExtraCreateParams = new Fido2ExtraCreateCredentialParams
|
||||||
|
(
|
||||||
|
callingRequest.GetClientDataHash(),
|
||||||
|
getRequest.CallingAppInfo?.PackageName
|
||||||
|
);
|
||||||
|
|
||||||
|
var fido2MediatorService = ServiceContainer.Resolve<IFido2MediatorService>();
|
||||||
|
var clientCreateCredentialResult = await fido2MediatorService.CreateCredentialAsync(credentialCreateParams, credentialExtraCreateParams);
|
||||||
|
if (clientCreateCredentialResult == null)
|
||||||
|
{
|
||||||
|
FailAndFinish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var transportsArray = new JSONArray();
|
||||||
|
if (clientCreateCredentialResult.Transports != null)
|
||||||
|
{
|
||||||
|
foreach (var transport in clientCreateCredentialResult.Transports)
|
||||||
|
{
|
||||||
|
transportsArray.Put(transport);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var responseInnerAndroidJson = new JSONObject();
|
||||||
|
if (clientCreateCredentialResult.ClientDataJSON != null)
|
||||||
|
{
|
||||||
|
responseInnerAndroidJson.Put("clientDataJSON", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.ClientDataJSON));
|
||||||
|
}
|
||||||
|
responseInnerAndroidJson.Put("authenticatorData", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.AuthData));
|
||||||
|
responseInnerAndroidJson.Put("attestationObject", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.AttestationObject));
|
||||||
|
responseInnerAndroidJson.Put("transports", transportsArray);
|
||||||
|
responseInnerAndroidJson.Put("publicKeyAlgorithm", clientCreateCredentialResult.PublicKeyAlgorithm);
|
||||||
|
responseInnerAndroidJson.Put("publicKey", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.PublicKey));
|
||||||
|
|
||||||
|
var rootAndroidJson = new JSONObject();
|
||||||
|
rootAndroidJson.Put("id", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.CredentialId));
|
||||||
|
rootAndroidJson.Put("rawId", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.CredentialId));
|
||||||
|
rootAndroidJson.Put("authenticatorAttachment", "platform");
|
||||||
|
rootAndroidJson.Put("type", "public-key");
|
||||||
|
rootAndroidJson.Put("clientExtensionResults", MapExtensionsToJson(clientCreateCredentialResult.Extensions));
|
||||||
|
rootAndroidJson.Put("response", responseInnerAndroidJson);
|
||||||
|
|
||||||
|
var result = new Intent();
|
||||||
|
var publicKeyResponse = new CreatePublicKeyCredentialResponse(rootAndroidJson.ToString());
|
||||||
|
PendingIntentHandler.SetCreateCredentialResponse(result, publicKeyResponse);
|
||||||
|
|
||||||
|
activity.SetResult(Result.Ok, result);
|
||||||
|
activity.Finish();
|
||||||
|
|
||||||
|
async Task DisplayAlertAsync(string title, string message)
|
||||||
|
{
|
||||||
|
if (ServiceContainer.TryResolve<IDeviceActionService>(out var deviceActionService))
|
||||||
|
{
|
||||||
|
await deviceActionService.DisplayAlertAsync(title, message, AppResources.Ok);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FailAndFinish()
|
||||||
|
{
|
||||||
|
var result = new Intent();
|
||||||
|
PendingIntentHandler.SetCreateCredentialException(result, new CreateCredentialUnknownException());
|
||||||
|
|
||||||
|
activity.SetResult(Result.Ok, result);
|
||||||
|
activity.Finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Fido2CreateCredentialExtensionsParams MapExtensionsFromJson(PublicKeyCredentialCreationOptions options)
|
||||||
|
{
|
||||||
|
if (options == null || !options.Json.Has("extensions"))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var extensions = options.Json.GetJSONObject("extensions");
|
||||||
|
return new Fido2CreateCredentialExtensionsParams
|
||||||
|
{
|
||||||
|
CredProps = extensions.Has("credProps") && extensions.GetBoolean("credProps")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JSONObject MapExtensionsToJson(Fido2CreateCredentialExtensionsResult extensions)
|
||||||
|
{
|
||||||
|
if (extensions == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var extensionsJson = new JSONObject();
|
||||||
|
if (extensions.CredProps != null)
|
||||||
|
{
|
||||||
|
var credPropsJson = new JSONObject();
|
||||||
|
credPropsJson.Put("rk", extensions.CredProps.Rk);
|
||||||
|
extensionsJson.Put("credProps", credPropsJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensionsJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> LoadFido2PrivilegedAllowedListAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = await FileSystem.OpenAppPackageFileAsync("fido2_privileged_allow_list.json");
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
|
||||||
|
return reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> ValidateCallingAppInfoAndGetOriginAsync(CallingAppInfo callingAppInfo, string rpId)
|
||||||
|
{
|
||||||
|
if (callingAppInfo.Origin is null)
|
||||||
|
{
|
||||||
|
return await ValidateAssetLinksAndGetOriginAsync(callingAppInfo, rpId);
|
||||||
|
}
|
||||||
|
|
||||||
|
var privilegedAllowedList = await LoadFido2PrivilegedAllowedListAsync();
|
||||||
|
if (privilegedAllowedList is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Could not load Fido2 privileged allowed list");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!privilegedAllowedList.Contains($"\"package_name\": \"{callingAppInfo.PackageName}\""))
|
||||||
|
{
|
||||||
|
throw new Core.Exceptions.ValidationException(AppResources.PasskeyOperationFailedBecauseBrowserIsNotPrivileged);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return callingAppInfo.GetOrigin(privilegedAllowedList);
|
||||||
|
}
|
||||||
|
catch (Java.Lang.IllegalStateException)
|
||||||
|
{
|
||||||
|
throw new Core.Exceptions.ValidationException(AppResources.PasskeyOperationFailedBecauseBrowserSignatureDoesNotMatch);
|
||||||
|
}
|
||||||
|
catch (Java.Lang.IllegalArgumentException)
|
||||||
|
{
|
||||||
|
return null; // wrong list format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> ValidateAssetLinksAndGetOriginAsync(CallingAppInfo callingAppInfo, string rpId)
|
||||||
|
{
|
||||||
|
if (!ServiceContainer.TryResolve<IAssetLinksService>(out var assetLinksService))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Can't resolve IAssetLinksService");
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedFingerprint = callingAppInfo.GetLatestCertificationFingerprint();
|
||||||
|
|
||||||
|
var isValid = await assetLinksService.ValidateAssetLinksAsync(rpId, callingAppInfo.PackageName, normalizedFingerprint);
|
||||||
|
|
||||||
|
return isValid ? callingAppInfo.GetAndroidOrigin() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
using Android.App;
|
||||||
|
using Android.Content;
|
||||||
|
using Android.Content.PM;
|
||||||
|
using Android.OS;
|
||||||
|
using AndroidX.Credentials;
|
||||||
|
using AndroidX.Credentials.Provider;
|
||||||
|
using AndroidX.Credentials.WebAuthn;
|
||||||
|
using Bit.App.Droid.Utilities;
|
||||||
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Core.Resources.Localization;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.App.Platforms.Android.Autofill;
|
||||||
|
using AndroidX.Credentials.Exceptions;
|
||||||
|
using Org.Json;
|
||||||
|
|
||||||
|
namespace Bit.Droid.Autofill
|
||||||
|
{
|
||||||
|
[Activity(
|
||||||
|
NoHistory = true,
|
||||||
|
LaunchMode = LaunchMode.SingleTop)]
|
||||||
|
public class CredentialProviderSelectionActivity : MauiAppCompatActivity
|
||||||
|
{
|
||||||
|
private LazyResolve<IFido2MediatorService> _fido2MediatorService = new LazyResolve<IFido2MediatorService>();
|
||||||
|
private LazyResolve<IFido2AndroidGetAssertionUserInterface> _fido2GetAssertionUserInterface = new LazyResolve<IFido2AndroidGetAssertionUserInterface>();
|
||||||
|
private LazyResolve<IVaultTimeoutService> _vaultTimeoutService = new LazyResolve<IVaultTimeoutService>();
|
||||||
|
private LazyResolve<IStateService> _stateService = new LazyResolve<IStateService>();
|
||||||
|
private LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
|
||||||
|
private LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
|
||||||
|
private LazyResolve<IDeviceActionService> _deviceActionService = new LazyResolve<IDeviceActionService>();
|
||||||
|
|
||||||
|
protected override void OnCreate(Bundle bundle)
|
||||||
|
{
|
||||||
|
Intent?.Validate();
|
||||||
|
base.OnCreate(bundle);
|
||||||
|
|
||||||
|
var cipherId = Intent?.GetStringExtra(CredentialProviderConstants.CredentialProviderCipherId);
|
||||||
|
if (string.IsNullOrEmpty(cipherId))
|
||||||
|
{
|
||||||
|
Finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetCipherAndPerformFido2AuthAsync(cipherId).FireAndForget();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Used to avoid crash on MAUI when doing back
|
||||||
|
public override void OnBackPressed()
|
||||||
|
{
|
||||||
|
Finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GetCipherAndPerformFido2AuthAsync(string cipherId)
|
||||||
|
{
|
||||||
|
string RpId = string.Empty;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(Intent);
|
||||||
|
|
||||||
|
if (getRequest is null)
|
||||||
|
{
|
||||||
|
FailAndFinish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var credentialOption = getRequest.CredentialOptions.FirstOrDefault();
|
||||||
|
var credentialPublic = credentialOption as GetPublicKeyCredentialOption;
|
||||||
|
|
||||||
|
var requestOptions = new PublicKeyCredentialRequestOptions(credentialPublic.RequestJson);
|
||||||
|
RpId = requestOptions.RpId;
|
||||||
|
|
||||||
|
var requestInfo = Intent.GetBundleExtra(CredentialProviderConstants.CredentialDataIntentExtra);
|
||||||
|
var credentialId = requestInfo?.GetByteArray(CredentialProviderConstants.CredentialIdIntentExtra);
|
||||||
|
var hasVaultBeenUnlockedInThisTransaction = Intent.GetBooleanExtra(CredentialProviderConstants.CredentialHasVaultBeenUnlockedInThisTransactionExtra, false);
|
||||||
|
|
||||||
|
var packageName = getRequest.CallingAppInfo.PackageName;
|
||||||
|
|
||||||
|
string origin;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
origin = await CredentialHelpers.ValidateCallingAppInfoAndGetOriginAsync(getRequest.CallingAppInfo, RpId);
|
||||||
|
}
|
||||||
|
catch (Core.Exceptions.ValidationException valEx)
|
||||||
|
{
|
||||||
|
await _deviceActionService.Value.DisplayAlertAsync(AppResources.AnErrorHasOccurred, valEx.Message, AppResources.Ok);
|
||||||
|
FailAndFinish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (origin is null)
|
||||||
|
{
|
||||||
|
await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, AppResources.PasskeysNotSupportedForThisApp, AppResources.Ok);
|
||||||
|
FailAndFinish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_fido2GetAssertionUserInterface.Value.Init(
|
||||||
|
cipherId,
|
||||||
|
false,
|
||||||
|
() => hasVaultBeenUnlockedInThisTransaction,
|
||||||
|
RpId
|
||||||
|
);
|
||||||
|
|
||||||
|
var clientAssertParams = new Fido2ClientAssertCredentialParams
|
||||||
|
{
|
||||||
|
Challenge = requestOptions.GetChallenge(),
|
||||||
|
RpId = RpId,
|
||||||
|
AllowCredentials = new Core.Utilities.Fido2.PublicKeyCredentialDescriptor[] { new Core.Utilities.Fido2.PublicKeyCredentialDescriptor { Id = credentialId } },
|
||||||
|
Origin = origin,
|
||||||
|
SameOriginWithAncestors = true,
|
||||||
|
UserVerification = requestOptions.UserVerification
|
||||||
|
};
|
||||||
|
|
||||||
|
var extraAssertParams = new Fido2ExtraAssertCredentialParams
|
||||||
|
(
|
||||||
|
getRequest.CallingAppInfo.Origin != null ? credentialPublic.GetClientDataHash() : null,
|
||||||
|
packageName
|
||||||
|
);
|
||||||
|
|
||||||
|
var assertResult = await _fido2MediatorService.Value.AssertCredentialAsync(clientAssertParams, extraAssertParams);
|
||||||
|
|
||||||
|
var result = new Intent();
|
||||||
|
|
||||||
|
var responseInnerAndroidJson = new JSONObject();
|
||||||
|
if (assertResult.ClientDataJSON != null)
|
||||||
|
{
|
||||||
|
responseInnerAndroidJson.Put("clientDataJSON", CoreHelpers.Base64UrlEncode(assertResult.ClientDataJSON));
|
||||||
|
}
|
||||||
|
responseInnerAndroidJson.Put("authenticatorData", CoreHelpers.Base64UrlEncode(assertResult.AuthenticatorData));
|
||||||
|
responseInnerAndroidJson.Put("signature", CoreHelpers.Base64UrlEncode(assertResult.Signature));
|
||||||
|
responseInnerAndroidJson.Put("userHandle", CoreHelpers.Base64UrlEncode(assertResult.SelectedCredential.UserHandle));
|
||||||
|
|
||||||
|
var rootAndroidJson = new JSONObject();
|
||||||
|
rootAndroidJson.Put("id", CoreHelpers.Base64UrlEncode(assertResult.SelectedCredential.Id));
|
||||||
|
rootAndroidJson.Put("rawId", CoreHelpers.Base64UrlEncode(assertResult.SelectedCredential.Id));
|
||||||
|
rootAndroidJson.Put("authenticatorAttachment", "platform");
|
||||||
|
rootAndroidJson.Put("type", "public-key");
|
||||||
|
rootAndroidJson.Put("clientExtensionResults", new JSONObject());
|
||||||
|
rootAndroidJson.Put("response", responseInnerAndroidJson);
|
||||||
|
|
||||||
|
var json = rootAndroidJson.ToString();
|
||||||
|
|
||||||
|
var cred = new PublicKeyCredential(json);
|
||||||
|
var credResponse = new GetCredentialResponse(cred);
|
||||||
|
PendingIntentHandler.SetGetCredentialResponse(result, credResponse);
|
||||||
|
|
||||||
|
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||||
|
{
|
||||||
|
SetResult(Result.Ok, result);
|
||||||
|
Finish();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (NotAllowedError)
|
||||||
|
{
|
||||||
|
await MainThread.InvokeOnMainThreadAsync(async () =>
|
||||||
|
{
|
||||||
|
await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, RpId), AppResources.Ok);
|
||||||
|
FailAndFinish();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
await MainThread.InvokeOnMainThreadAsync(async () =>
|
||||||
|
{
|
||||||
|
await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, RpId), AppResources.Ok);
|
||||||
|
FailAndFinish();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FailAndFinish()
|
||||||
|
{
|
||||||
|
var result = new Intent();
|
||||||
|
PendingIntentHandler.SetGetCredentialException(result, new GetCredentialUnknownException());
|
||||||
|
|
||||||
|
SetResult(Result.Ok, result);
|
||||||
|
Finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
168
src/App/Platforms/Android/Autofill/CredentialProviderService.cs
Normal file
168
src/App/Platforms/Android/Autofill/CredentialProviderService.cs
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
using Android;
|
||||||
|
using Android.App;
|
||||||
|
using Android.Content;
|
||||||
|
using Android.OS;
|
||||||
|
using Android.Runtime;
|
||||||
|
using AndroidX.Credentials.Provider;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using AndroidX.Credentials.Exceptions;
|
||||||
|
using Bit.App.Droid.Utilities;
|
||||||
|
using Bit.Core.Resources.Localization;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
public const string GetFido2IntentAction = "PACKAGE_NAME.GET_PASSKEY";
|
||||||
|
public const string CreateFido2IntentAction = "PACKAGE_NAME.CREATE_PASSKEY";
|
||||||
|
public const int UniqueGetRequestCode = 94556023;
|
||||||
|
public const int UniqueCreateRequestCode = 94556024;
|
||||||
|
|
||||||
|
private readonly LazyResolve<IVaultTimeoutService> _vaultTimeoutService = new LazyResolve<IVaultTimeoutService>();
|
||||||
|
private readonly LazyResolve<IStateService> _stateService = new LazyResolve<IStateService>();
|
||||||
|
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||||
|
|
||||||
|
public override async void OnBeginCreateCredentialRequest(BeginCreateCredentialRequest request,
|
||||||
|
CancellationSignal cancellationSignal, IOutcomeReceiver callback)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await ProcessCreateCredentialsRequestAsync(request);
|
||||||
|
if (response != null)
|
||||||
|
{
|
||||||
|
await MainThread.InvokeOnMainThreadAsync(() => callback.OnResult(response));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Value.Exception(ex);
|
||||||
|
}
|
||||||
|
MainThread.BeginInvokeOnMainThread(() => callback.OnError(AppResources.ErrorCreatingPasskey));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async void OnBeginGetCredentialRequest(BeginGetCredentialRequest request,
|
||||||
|
CancellationSignal cancellationSignal, IOutcomeReceiver callback)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _vaultTimeoutService.Value.CheckVaultTimeoutAsync();
|
||||||
|
var locked = await _vaultTimeoutService.Value.IsLockedAsync();
|
||||||
|
if (!locked)
|
||||||
|
{
|
||||||
|
var response = await ProcessGetCredentialsRequestAsync(request);
|
||||||
|
callback.OnResult(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var intent = new Intent(ApplicationContext, typeof(MainActivity));
|
||||||
|
intent.PutExtra(CredentialProviderConstants.Fido2CredentialAction, CredentialProviderConstants.Fido2CredentialGet);
|
||||||
|
var pendingIntent = PendingIntent.GetActivity(ApplicationContext, UniqueGetRequestCode, intent,
|
||||||
|
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true));
|
||||||
|
|
||||||
|
var unlockAction = new AuthenticationAction(AppResources.Unlock, pendingIntent);
|
||||||
|
|
||||||
|
var unlockResponse = new BeginGetCredentialResponse.Builder()
|
||||||
|
.SetAuthenticationActions(new List<AuthenticationAction>() { unlockAction } )
|
||||||
|
.Build();
|
||||||
|
callback.OnResult(unlockResponse);
|
||||||
|
}
|
||||||
|
catch (GetCredentialException e)
|
||||||
|
{
|
||||||
|
_logger.Value.Exception(e);
|
||||||
|
callback.OnError(e.ErrorMessage ?? AppResources.ErrorReadingPasskey);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.Value.Exception(e);
|
||||||
|
callback.OnError(AppResources.ErrorReadingPasskey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<BeginCreateCredentialResponse> ProcessCreateCredentialsRequestAsync(
|
||||||
|
BeginCreateCredentialRequest request)
|
||||||
|
{
|
||||||
|
if (request == null) { return null; }
|
||||||
|
|
||||||
|
if (request is BeginCreatePasswordCredentialRequest beginCreatePasswordCredentialRequest)
|
||||||
|
{
|
||||||
|
//This flow can be used if Password flow needs to be implemented
|
||||||
|
throw new NotImplementedException();
|
||||||
|
//return HandleCreatePasswordQuery(beginCreatePasswordCredentialRequest);
|
||||||
|
}
|
||||||
|
else if (request is BeginCreatePublicKeyCredentialRequest beginCreatePublicKeyCredentialRequest)
|
||||||
|
{
|
||||||
|
return await HandleCreatePasskeyQueryAsync(beginCreatePublicKeyCredentialRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<BeginCreateCredentialResponse> HandleCreatePasskeyQueryAsync(BeginCreatePublicKeyCredentialRequest optionRequest)
|
||||||
|
{
|
||||||
|
var intent = new Intent(ApplicationContext, typeof(MainActivity));
|
||||||
|
intent.PutExtra(CredentialProviderConstants.Fido2CredentialAction, CredentialProviderConstants.Fido2CredentialCreate);
|
||||||
|
var pendingIntent = PendingIntent.GetActivity(ApplicationContext, UniqueCreateRequestCode, intent,
|
||||||
|
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true));
|
||||||
|
|
||||||
|
var userEmail = await GetSafeActiveAccountEmailAsync();
|
||||||
|
|
||||||
|
var createEntryBuilder = new CreateEntry.Builder(userEmail ?? AppResources.Bitwarden, pendingIntent)
|
||||||
|
.SetDescription(userEmail != null
|
||||||
|
? string.Format(AppResources.YourPasskeyWillBeSavedToYourBitwardenVaultForX, userEmail)
|
||||||
|
: AppResources.YourPasskeyWillBeSavedToYourBitwardenVault)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var createCredentialResponse = new BeginCreateCredentialResponse.Builder()
|
||||||
|
.AddCreateEntry(createEntryBuilder);
|
||||||
|
|
||||||
|
return createCredentialResponse.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<BeginGetCredentialResponse> ProcessGetCredentialsRequestAsync(
|
||||||
|
BeginGetCredentialRequest request)
|
||||||
|
{
|
||||||
|
var credentialEntries = new List<CredentialEntry>();
|
||||||
|
|
||||||
|
foreach (var option in request.BeginGetCredentialOptions.OfType<BeginGetPublicKeyCredentialOption>())
|
||||||
|
{
|
||||||
|
credentialEntries.AddRange(await Bit.App.Platforms.Android.Autofill.CredentialHelpers.PopulatePasskeyDataAsync(request.CallingAppInfo, option, ApplicationContext, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!credentialEntries.Any())
|
||||||
|
{
|
||||||
|
return new BeginGetCredentialResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BeginGetCredentialResponse.Builder()
|
||||||
|
.SetCredentialEntries(credentialEntries)
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnClearCredentialStateRequest(ProviderClearCredentialStateRequest request,
|
||||||
|
CancellationSignal cancellationSignal, IOutcomeReceiver callback)
|
||||||
|
{
|
||||||
|
callback.OnResult(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetSafeActiveAccountEmailAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _stateService.Value.GetEmailAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// if it throws to get the user's email then we log and continue showing a more generic message
|
||||||
|
_logger.Value.Exception(ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
|
namespace Bit.App.Platforms.Android.Autofill
|
||||||
|
{
|
||||||
|
public interface IFido2AndroidGetAssertionUserInterface : IFido2GetAssertionUserInterface
|
||||||
|
{
|
||||||
|
void Init(string cipherId,
|
||||||
|
bool userVerified,
|
||||||
|
Func<bool> hasVaultBeenUnlockedInThisTransaction,
|
||||||
|
string rpId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Fido2GetAssertionUserInterface : Core.Utilities.Fido2.Fido2GetAssertionUserInterface, IFido2AndroidGetAssertionUserInterface
|
||||||
|
{
|
||||||
|
private readonly IStateService _stateService;
|
||||||
|
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||||
|
private readonly ICipherService _cipherService;
|
||||||
|
private readonly IUserVerificationMediatorService _userVerificationMediatorService;
|
||||||
|
|
||||||
|
public Fido2GetAssertionUserInterface(IStateService stateService,
|
||||||
|
IVaultTimeoutService vaultTimeoutService,
|
||||||
|
ICipherService cipherService,
|
||||||
|
IUserVerificationMediatorService userVerificationMediatorService)
|
||||||
|
{
|
||||||
|
_stateService = stateService;
|
||||||
|
_vaultTimeoutService = vaultTimeoutService;
|
||||||
|
_cipherService = cipherService;
|
||||||
|
_userVerificationMediatorService = userVerificationMediatorService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Init(string cipherId,
|
||||||
|
bool userVerified,
|
||||||
|
Func<bool> hasVaultBeenUnlockedInThisTransaction,
|
||||||
|
string rpId)
|
||||||
|
{
|
||||||
|
Init(cipherId,
|
||||||
|
userVerified,
|
||||||
|
EnsureAuthenAndVaultUnlockedAsync,
|
||||||
|
hasVaultBeenUnlockedInThisTransaction,
|
||||||
|
(cipherId, userVerificationPreference) => VerifyUserAsync(cipherId, userVerificationPreference, rpId, hasVaultBeenUnlockedInThisTransaction()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EnsureAuthenAndVaultUnlockedAsync()
|
||||||
|
{
|
||||||
|
if (!await _stateService.IsAuthenticatedAsync() || await _vaultTimeoutService.IsLockedAsync())
|
||||||
|
{
|
||||||
|
// this should never happen but just in case.
|
||||||
|
throw new InvalidOperationException("Not authed or vault locked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference, string rpId, bool vaultUnlockedDuringThisTransaction)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var encrypted = await _cipherService.GetAsync(selectedCipherId);
|
||||||
|
var cipher = await encrypted.DecryptAsync();
|
||||||
|
|
||||||
|
var userVerification = await _userVerificationMediatorService.VerifyUserForFido2Async(
|
||||||
|
new Fido2UserVerificationOptions(
|
||||||
|
cipher?.Reprompt == Core.Enums.CipherRepromptType.Password,
|
||||||
|
userVerificationPreference,
|
||||||
|
vaultUnlockedDuringThisTransaction,
|
||||||
|
rpId)
|
||||||
|
);
|
||||||
|
return !userVerification.IsCancelled && userVerification.Result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Resources.Localization;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
|
namespace Bit.App.Platforms.Android.Autofill
|
||||||
|
{
|
||||||
|
public class Fido2MakeCredentialUserInterface : IFido2MakeCredentialConfirmationUserInterface
|
||||||
|
{
|
||||||
|
private readonly IStateService _stateService;
|
||||||
|
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||||
|
private readonly ICipherService _cipherService;
|
||||||
|
private readonly IUserVerificationMediatorService _userVerificationMediatorService;
|
||||||
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
|
private LazyResolve<IMessagingService> _messagingService = new LazyResolve<IMessagingService>();
|
||||||
|
|
||||||
|
private TaskCompletionSource<(string cipherId, bool? userVerified)> _confirmCredentialTcs;
|
||||||
|
private TaskCompletionSource<bool> _unlockVaultTcs;
|
||||||
|
private Fido2UserVerificationOptions? _currentDefaultUserVerificationOptions;
|
||||||
|
private Func<bool> _checkHasVaultBeenUnlockedInThisTransaction;
|
||||||
|
|
||||||
|
public Fido2MakeCredentialUserInterface(IStateService stateService,
|
||||||
|
IVaultTimeoutService vaultTimeoutService,
|
||||||
|
ICipherService cipherService,
|
||||||
|
IUserVerificationMediatorService userVerificationMediatorService,
|
||||||
|
IDeviceActionService deviceActionService,
|
||||||
|
IPlatformUtilsService platformUtilsService)
|
||||||
|
{
|
||||||
|
_stateService = stateService;
|
||||||
|
_vaultTimeoutService = vaultTimeoutService;
|
||||||
|
_cipherService = cipherService;
|
||||||
|
_userVerificationMediatorService = userVerificationMediatorService;
|
||||||
|
_deviceActionService = deviceActionService;
|
||||||
|
_platformUtilsService = platformUtilsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasVaultBeenUnlockedInThisTransaction => _checkHasVaultBeenUnlockedInThisTransaction?.Invoke() == true;
|
||||||
|
|
||||||
|
public bool IsConfirmingNewCredential => _confirmCredentialTcs?.Task != null && !_confirmCredentialTcs.Task.IsCompleted;
|
||||||
|
public bool IsWaitingUnlockVault => _unlockVaultTcs?.Task != null && !_unlockVaultTcs.Task.IsCompleted;
|
||||||
|
|
||||||
|
public async Task<(string CipherId, bool UserVerified)> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams)
|
||||||
|
{
|
||||||
|
_confirmCredentialTcs?.TrySetCanceled();
|
||||||
|
_confirmCredentialTcs = null;
|
||||||
|
_confirmCredentialTcs = new TaskCompletionSource<(string cipherId, bool? userVerified)>();
|
||||||
|
|
||||||
|
_currentDefaultUserVerificationOptions = new Fido2UserVerificationOptions(false, confirmNewCredentialParams.UserVerificationPreference, HasVaultBeenUnlockedInThisTransaction, confirmNewCredentialParams.RpId);
|
||||||
|
|
||||||
|
_messagingService.Value.Send(Bit.Core.Constants.CredentialNavigateToAutofillCipherMessageCommand, confirmNewCredentialParams);
|
||||||
|
|
||||||
|
var (cipherId, isUserVerified) = await _confirmCredentialTcs.Task;
|
||||||
|
|
||||||
|
var verified = isUserVerified;
|
||||||
|
if (verified is null)
|
||||||
|
{
|
||||||
|
var userVerification = await VerifyUserAsync(cipherId, confirmNewCredentialParams.UserVerificationPreference, confirmNewCredentialParams.RpId);
|
||||||
|
// TODO: If cancelled then let the user choose another cipher.
|
||||||
|
// I think this can be done by showing a message to the uesr and recursive calling of this method ConfirmNewCredentialAsync
|
||||||
|
verified = !userVerification.IsCancelled && userVerification.Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cipherId is null)
|
||||||
|
{
|
||||||
|
return await CreateNewLoginForFido2CredentialAsync(confirmNewCredentialParams, verified.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (cipherId, verified.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(string CipherId, bool UserVerified)> CreateNewLoginForFido2CredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams, bool userVerified)
|
||||||
|
{
|
||||||
|
if (!userVerified && await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
confirmNewCredentialParams.UserVerificationPreference,
|
||||||
|
true,
|
||||||
|
confirmNewCredentialParams.RpId
|
||||||
|
)))
|
||||||
|
{
|
||||||
|
return (null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||||
|
|
||||||
|
var cipherId = await _cipherService.CreateNewLoginForPasskeyAsync(confirmNewCredentialParams);
|
||||||
|
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
|
||||||
|
return (cipherId, userVerified);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EnsureUnlockedVaultAsync()
|
||||||
|
{
|
||||||
|
if (!await _stateService.IsAuthenticatedAsync()
|
||||||
|
||
|
||||||
|
await _vaultTimeoutService.IsLoggedOutByTimeoutAsync()
|
||||||
|
||
|
||||||
|
await _vaultTimeoutService.ShouldLogOutByTimeoutAsync())
|
||||||
|
{
|
||||||
|
await NavigateAndWaitForUnlockAsync(Bit.Core.Enums.NavigationTarget.HomeLogin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await _vaultTimeoutService.IsLockedAsync())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await NavigateAndWaitForUnlockAsync(Bit.Core.Enums.NavigationTarget.Lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task NavigateAndWaitForUnlockAsync(Bit.Core.Enums.NavigationTarget navTarget)
|
||||||
|
{
|
||||||
|
_unlockVaultTcs?.TrySetCanceled();
|
||||||
|
_unlockVaultTcs = new TaskCompletionSource<bool>();
|
||||||
|
|
||||||
|
_messagingService.Value.Send(Bit.Core.Constants.NavigateToMessageCommand, navTarget);
|
||||||
|
|
||||||
|
await _unlockVaultTcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task InformExcludedCredentialAsync(string[] existingCipherIds)
|
||||||
|
{
|
||||||
|
// TODO: Show excluded credential to the user in some screen.
|
||||||
|
return Task.FromResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCheckHasVaultBeenUnlockedInThisTransaction(Func<bool> checkHasVaultBeenUnlockedInThisTransaction)
|
||||||
|
{
|
||||||
|
_checkHasVaultBeenUnlockedInThisTransaction = checkHasVaultBeenUnlockedInThisTransaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Confirm(string cipherId, bool? userVerified) => _confirmCredentialTcs?.TrySetResult((cipherId, userVerified));
|
||||||
|
public void ConfirmVaultUnlocked() => _unlockVaultTcs?.TrySetResult(true);
|
||||||
|
|
||||||
|
public async Task ConfirmAsync(string cipherId, bool alreadyHasFido2Credential, bool? userVerified)
|
||||||
|
{
|
||||||
|
if (alreadyHasFido2Credential
|
||||||
|
&&
|
||||||
|
!await _platformUtilsService.ShowDialogAsync(
|
||||||
|
AppResources.ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey,
|
||||||
|
AppResources.OverwritePasskey,
|
||||||
|
AppResources.Yes,
|
||||||
|
AppResources.No))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Confirm(cipherId, userVerified);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel() => _confirmCredentialTcs?.TrySetCanceled();
|
||||||
|
|
||||||
|
public void OnConfirmationException(Exception ex) => _confirmCredentialTcs?.TrySetException(ex);
|
||||||
|
|
||||||
|
private async Task<CancellableResult<bool>> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference, string rpId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (selectedCipherId is null && userVerificationPreference == Fido2UserVerificationPreference.Discouraged)
|
||||||
|
{
|
||||||
|
return new CancellableResult<bool>(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldCheckMasterPasswordReprompt = false;
|
||||||
|
if (selectedCipherId != null)
|
||||||
|
{
|
||||||
|
var encrypted = await _cipherService.GetAsync(selectedCipherId);
|
||||||
|
var cipher = await encrypted.DecryptAsync();
|
||||||
|
shouldCheckMasterPasswordReprompt = cipher?.Reprompt == Core.Enums.CipherRepromptType.Password;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _userVerificationMediatorService.VerifyUserForFido2Async(
|
||||||
|
new Fido2UserVerificationOptions(
|
||||||
|
shouldCheckMasterPasswordReprompt,
|
||||||
|
userVerificationPreference,
|
||||||
|
HasVaultBeenUnlockedInThisTransaction,
|
||||||
|
rpId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
return new CancellableResult<bool>(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Fido2UserVerificationOptions? GetCurrentUserVerificationOptions() => _currentDefaultUserVerificationOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ using Bit.App.Droid.Utilities;
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using FileProvider = AndroidX.Core.Content.FileProvider;
|
using FileProvider = AndroidX.Core.Content.FileProvider;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
namespace Bit.Droid
|
namespace Bit.Droid
|
||||||
{
|
{
|
||||||
@@ -167,6 +168,13 @@ namespace Bit.Droid
|
|||||||
base.OnNewIntent(intent);
|
base.OnNewIntent(intent);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (intent?.GetStringExtra(CredentialProviderConstants.Fido2CredentialAction) == CredentialProviderConstants.Fido2CredentialCreate
|
||||||
|
&&
|
||||||
|
_appOptions != null)
|
||||||
|
{
|
||||||
|
_appOptions.HasUnlockedInThisTransaction = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (intent?.GetStringExtra("uri") is string uri)
|
if (intent?.GetStringExtra("uri") is string uri)
|
||||||
{
|
{
|
||||||
_messagingService.Send(App.App.POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE);
|
_messagingService.Send(App.App.POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE);
|
||||||
@@ -325,12 +333,15 @@ namespace Bit.Droid
|
|||||||
|
|
||||||
private AppOptions GetOptions()
|
private AppOptions GetOptions()
|
||||||
{
|
{
|
||||||
|
var fido2CredentialAction = Intent.GetStringExtra(CredentialProviderConstants.Fido2CredentialAction);
|
||||||
var options = new AppOptions
|
var options = new AppOptions
|
||||||
{
|
{
|
||||||
Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra(AutofillConstants.AutofillFrameworkUri),
|
Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra(AutofillConstants.AutofillFrameworkUri),
|
||||||
MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false),
|
MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false),
|
||||||
GeneratorTile = Intent.GetBooleanExtra("generatorTile", false),
|
GeneratorTile = Intent.GetBooleanExtra("generatorTile", false),
|
||||||
FromAutofillFramework = Intent.GetBooleanExtra(AutofillConstants.AutofillFramework, false),
|
FromAutofillFramework = Intent.GetBooleanExtra(AutofillConstants.AutofillFramework, false),
|
||||||
|
Fido2CredentialAction = fido2CredentialAction,
|
||||||
|
FromFido2Framework = !string.IsNullOrWhiteSpace(fido2CredentialAction),
|
||||||
CreateSend = GetCreateSendRequest(Intent)
|
CreateSend = GetCreateSendRequest(Intent)
|
||||||
};
|
};
|
||||||
var fillType = Intent.GetIntExtra(AutofillConstants.AutofillFrameworkFillType, 0);
|
var fillType = Intent.GetIntExtra(AutofillConstants.AutofillFrameworkFillType, 0);
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ using Bit.App.Utilities;
|
|||||||
using Bit.App.Pages;
|
using Bit.App.Pages;
|
||||||
using Bit.App.Utilities.AccountManagement;
|
using Bit.App.Utilities.AccountManagement;
|
||||||
using Bit.App.Controls;
|
using Bit.App.Controls;
|
||||||
|
using Bit.App.Platforms.Android.Autofill;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Services.UserVerification;
|
||||||
|
|
||||||
#if !FDROID
|
#if !FDROID
|
||||||
using Android.Gms.Security;
|
using Android.Gms.Security;
|
||||||
#endif
|
#endif
|
||||||
@@ -85,6 +88,57 @@ namespace Bit.Droid
|
|||||||
ServiceContainer.Resolve<IWatchDeviceService>(),
|
ServiceContainer.Resolve<IWatchDeviceService>(),
|
||||||
ServiceContainer.Resolve<IConditionedAwaiterManager>());
|
ServiceContainer.Resolve<IConditionedAwaiterManager>());
|
||||||
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
||||||
|
|
||||||
|
var userPinService = new UserPinService(
|
||||||
|
ServiceContainer.Resolve<IStateService>(),
|
||||||
|
ServiceContainer.Resolve<ICryptoService>(),
|
||||||
|
ServiceContainer.Resolve<IVaultTimeoutService>());
|
||||||
|
ServiceContainer.Register<IUserPinService>(userPinService);
|
||||||
|
|
||||||
|
var userVerificationMediatorService = new UserVerificationMediatorService(
|
||||||
|
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
|
||||||
|
ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService"),
|
||||||
|
userPinService,
|
||||||
|
deviceActionService,
|
||||||
|
ServiceContainer.Resolve<IUserVerificationService>());
|
||||||
|
ServiceContainer.Register<IUserVerificationMediatorService>(userVerificationMediatorService);
|
||||||
|
|
||||||
|
var fido2AuthenticatorService = new Fido2AuthenticatorService(
|
||||||
|
ServiceContainer.Resolve<ICipherService>(),
|
||||||
|
ServiceContainer.Resolve<ISyncService>(),
|
||||||
|
ServiceContainer.Resolve<ICryptoFunctionService>(),
|
||||||
|
userVerificationMediatorService);
|
||||||
|
ServiceContainer.Register<IFido2AuthenticatorService>(fido2AuthenticatorService);
|
||||||
|
|
||||||
|
var fido2GetAssertionUserInterface = new Fido2GetAssertionUserInterface(
|
||||||
|
ServiceContainer.Resolve<IStateService>(),
|
||||||
|
ServiceContainer.Resolve<IVaultTimeoutService>(),
|
||||||
|
ServiceContainer.Resolve<ICipherService>(),
|
||||||
|
ServiceContainer.Resolve<IUserVerificationMediatorService>());
|
||||||
|
ServiceContainer.Register<IFido2AndroidGetAssertionUserInterface>(fido2GetAssertionUserInterface);
|
||||||
|
|
||||||
|
var fido2MakeCredentialUserInterface = new Fido2MakeCredentialUserInterface(
|
||||||
|
ServiceContainer.Resolve<IStateService>(),
|
||||||
|
ServiceContainer.Resolve<IVaultTimeoutService>(),
|
||||||
|
ServiceContainer.Resolve<ICipherService>(),
|
||||||
|
ServiceContainer.Resolve<IUserVerificationMediatorService>(),
|
||||||
|
ServiceContainer.Resolve<IDeviceActionService>(),
|
||||||
|
ServiceContainer.Resolve<IPlatformUtilsService>());
|
||||||
|
ServiceContainer.Register<IFido2MakeCredentialConfirmationUserInterface>(fido2MakeCredentialUserInterface);
|
||||||
|
|
||||||
|
var fido2ClientService = new Fido2ClientService(
|
||||||
|
ServiceContainer.Resolve<IStateService>(),
|
||||||
|
ServiceContainer.Resolve<IEnvironmentService>(),
|
||||||
|
ServiceContainer.Resolve<ICryptoFunctionService>(),
|
||||||
|
ServiceContainer.Resolve<IFido2AuthenticatorService>(),
|
||||||
|
fido2GetAssertionUserInterface,
|
||||||
|
fido2MakeCredentialUserInterface);
|
||||||
|
ServiceContainer.Register<IFido2ClientService>(fido2ClientService);
|
||||||
|
|
||||||
|
ServiceContainer.Register<IFido2MediatorService>(new Fido2MediatorService(
|
||||||
|
fido2AuthenticatorService,
|
||||||
|
fido2ClientService,
|
||||||
|
ServiceContainer.Resolve<ICipherService>()));
|
||||||
}
|
}
|
||||||
#if !FDROID
|
#if !FDROID
|
||||||
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
|
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
|
||||||
@@ -160,7 +214,6 @@ namespace Bit.Droid
|
|||||||
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
|
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
|
||||||
var cryptoService = new CryptoService(stateService, cryptoFunctionService, logger);
|
var cryptoService = new CryptoService(stateService, cryptoFunctionService, logger);
|
||||||
var biometricService = new BiometricService(stateService, cryptoService);
|
var biometricService = new BiometricService(stateService, cryptoService);
|
||||||
var userPinService = new UserPinService(stateService, cryptoService);
|
|
||||||
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService, stateService);
|
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService, stateService);
|
||||||
|
|
||||||
ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage);
|
ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage);
|
||||||
@@ -184,7 +237,6 @@ namespace Bit.Droid
|
|||||||
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
|
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
|
||||||
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
|
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
|
||||||
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
|
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
|
||||||
ServiceContainer.Register<IUserPinService>(userPinService);
|
|
||||||
|
|
||||||
// Push
|
// Push
|
||||||
#if FDROID
|
#if FDROID
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#if !FDROID
|
#if !FDROID
|
||||||
using System;
|
using System;
|
||||||
using Android.App;
|
using Android.App;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Firebase.Messaging;
|
using Firebase.Messaging;
|
||||||
@@ -20,7 +21,7 @@ namespace Bit.Droid.Push
|
|||||||
try {
|
try {
|
||||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
|
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
|
||||||
|
|
||||||
await stateService.SetPushRegisteredTokenAsync(token);
|
await stateService.SetPushRegisteredTokenAsync(token);
|
||||||
await pushNotificationService.RegisterAsync();
|
await pushNotificationService.RegisterAsync();
|
||||||
}
|
}
|
||||||
@@ -38,13 +39,33 @@ namespace Bit.Droid.Push
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var data = message.Data.ContainsKey("data") ? message.Data["data"] : null;
|
|
||||||
if (data == null)
|
JObject obj = null;
|
||||||
|
if (message.Data.TryGetValue("data", out var data))
|
||||||
|
{
|
||||||
|
// Legacy GCM format
|
||||||
|
obj = JObject.Parse(data);
|
||||||
|
}
|
||||||
|
else if (message.Data.TryGetValue("type", out var typeData) &&
|
||||||
|
Enum.TryParse(typeData, out NotificationType type))
|
||||||
|
{
|
||||||
|
// New FCMv1 format
|
||||||
|
obj = new JObject
|
||||||
|
{
|
||||||
|
{ "type", (int)type }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (message.Data.TryGetValue("payload", out var payloadData))
|
||||||
|
{
|
||||||
|
obj.Add("payload", payloadData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var obj = JObject.Parse(data);
|
|
||||||
var listener = ServiceContainer.Resolve<IPushNotificationListenerService>(
|
var listener = ServiceContainer.Resolve<IPushNotificationListenerService>(
|
||||||
"pushNotificationListenerService");
|
"pushNotificationListenerService");
|
||||||
await listener.OnMessageAsync(obj, Device.Android);
|
await listener.OnMessageAsync(obj, Device.Android);
|
||||||
|
|||||||
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>
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
using System.Linq;
|
using Android.App;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Android.App;
|
|
||||||
using Android.App.Assist;
|
using Android.App.Assist;
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
|
using Android.Credentials;
|
||||||
using Android.OS;
|
using Android.OS;
|
||||||
using Android.Provider;
|
using Android.Provider;
|
||||||
using Android.Views.Autofill;
|
using Android.Views.Autofill;
|
||||||
|
using Bit.App.Abstractions;
|
||||||
using Bit.Core.Resources.Localization;
|
using Bit.Core.Resources.Localization;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@@ -37,6 +37,42 @@ namespace Bit.Droid.Services
|
|||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool CredentialProviderServiceEnabled()
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SdkInt < BuildVersionCodes.UpsideDownCake)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)Platform.CurrentActivity;
|
||||||
|
if (activity == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var credManager = activity.GetSystemService(Java.Lang.Class.FromType(typeof(CredentialManager))) as CredentialManager;
|
||||||
|
if (credManager == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var credentialProviderServiceComponentName = new ComponentName(activity, Java.Lang.Class.FromType(typeof(CredentialProviderService)));
|
||||||
|
return credManager.IsEnabledCredentialProviderService(credentialProviderServiceComponentName);
|
||||||
|
}
|
||||||
|
catch (Java.Lang.NullPointerException)
|
||||||
|
{
|
||||||
|
// CredentialManager API is not working fully and may return a NullPointerException even if the CredentialProviderService is working and enabled
|
||||||
|
// Info Here: https://developer.android.com/reference/android/credentials/CredentialManager#isEnabledCredentialProviderService(android.content.ComponentName)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool AutofillServiceEnabled()
|
public bool AutofillServiceEnabled()
|
||||||
{
|
{
|
||||||
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
||||||
@@ -163,7 +199,17 @@ namespace Bit.Droid.Services
|
|||||||
return Accessibility.AccessibilityHelpers.OverlayPermitted();
|
return Accessibility.AccessibilityHelpers.OverlayPermitted();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void DisableCredentialProviderService()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// We should try to find a way to programmatically disable the provider service when the API allows for it.
|
||||||
|
// For now we'll take the user to Credential Settings so they can manually disable it
|
||||||
|
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||||
|
deviceActionService.OpenCredentialProviderSettings();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
public void DisableAutofillService()
|
public void DisableAutofillService()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using Android.App;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Android.App;
|
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.Content.PM;
|
using Android.Content.PM;
|
||||||
using Android.Nfc;
|
using Android.Nfc;
|
||||||
@@ -11,16 +9,20 @@ using Android.Text.Method;
|
|||||||
using Android.Views;
|
using Android.Views;
|
||||||
using Android.Views.InputMethods;
|
using Android.Views.InputMethods;
|
||||||
using Android.Widget;
|
using Android.Widget;
|
||||||
|
using AndroidX.Credentials;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.Core.Resources.Localization;
|
using Bit.Core.Resources.Localization;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.App.Utilities.Prompts;
|
using Bit.App.Utilities.Prompts;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.App.Droid.Utilities;
|
using Bit.App.Droid.Utilities;
|
||||||
|
using Bit.App.Models;
|
||||||
|
using Bit.Droid.Autofill;
|
||||||
using Microsoft.Maui.Controls.Compatibility.Platform.Android;
|
using Microsoft.Maui.Controls.Compatibility.Platform.Android;
|
||||||
using Resource = Bit.Core.Resource;
|
using Resource = Bit.Core.Resource;
|
||||||
using Application = Android.App.Application;
|
using Application = Android.App.Application;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
namespace Bit.Droid.Services
|
namespace Bit.Droid.Services
|
||||||
{
|
{
|
||||||
@@ -72,17 +74,28 @@ namespace Bit.Droid.Services
|
|||||||
|
|
||||||
public bool LaunchApp(string appName)
|
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;
|
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)
|
public async Task ShowLoadingAsync(string text)
|
||||||
@@ -192,7 +205,7 @@ namespace Bit.Droid.Services
|
|||||||
string text = null, string okButtonText = null, string cancelButtonText = null,
|
string text = null, string okButtonText = null, string cancelButtonText = null,
|
||||||
bool numericKeyboard = false, bool autofocus = true, bool password = false)
|
bool numericKeyboard = false, bool autofocus = true, bool password = false)
|
||||||
{
|
{
|
||||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||||
if (activity == null)
|
if (activity == null)
|
||||||
{
|
{
|
||||||
return Task.FromResult<string>(null);
|
return Task.FromResult<string>(null);
|
||||||
@@ -249,7 +262,7 @@ namespace Bit.Droid.Services
|
|||||||
|
|
||||||
public Task<ValidatablePromptResponse?> DisplayValidatablePromptAsync(ValidatablePromptConfig config)
|
public Task<ValidatablePromptResponse?> DisplayValidatablePromptAsync(ValidatablePromptConfig config)
|
||||||
{
|
{
|
||||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||||
if (activity == null)
|
if (activity == null)
|
||||||
{
|
{
|
||||||
return Task.FromResult<ValidatablePromptResponse?>(null);
|
return Task.FromResult<ValidatablePromptResponse?>(null);
|
||||||
@@ -326,7 +339,7 @@ namespace Bit.Droid.Services
|
|||||||
|
|
||||||
public void RateApp()
|
public void RateApp()
|
||||||
{
|
{
|
||||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var rateIntent = RateIntentForUrl("market://details", activity);
|
var rateIntent = RateIntentForUrl("market://details", activity);
|
||||||
@@ -359,14 +372,14 @@ namespace Bit.Droid.Services
|
|||||||
|
|
||||||
public bool SupportsNfc()
|
public bool SupportsNfc()
|
||||||
{
|
{
|
||||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||||
var manager = activity.GetSystemService(Context.NfcService) as NfcManager;
|
var manager = activity.GetSystemService(Context.NfcService) as NfcManager;
|
||||||
return manager.DefaultAdapter?.IsEnabled ?? false;
|
return manager.DefaultAdapter?.IsEnabled ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool SupportsCamera()
|
public bool SupportsCamera()
|
||||||
{
|
{
|
||||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||||
return activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera);
|
return activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,7 +395,7 @@ namespace Bit.Droid.Services
|
|||||||
|
|
||||||
public Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons)
|
public Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons)
|
||||||
{
|
{
|
||||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||||
if (activity == null)
|
if (activity == null)
|
||||||
{
|
{
|
||||||
return Task.FromResult<string>(null);
|
return Task.FromResult<string>(null);
|
||||||
@@ -463,7 +476,7 @@ namespace Bit.Droid.Services
|
|||||||
|
|
||||||
public void OpenAccessibilityOverlayPermissionSettings()
|
public void OpenAccessibilityOverlayPermissionSettings()
|
||||||
{
|
{
|
||||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var intent = new Intent(Settings.ActionManageOverlayPermission);
|
var intent = new Intent(Settings.ActionManageOverlayPermission);
|
||||||
@@ -490,11 +503,32 @@ namespace Bit.Droid.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OpenCredentialProviderSettings()
|
||||||
|
{
|
||||||
|
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pendingIntent = ICredentialManager.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()
|
public void OpenAccessibilitySettings()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||||
var intent = new Intent(Settings.ActionAccessibilitySettings);
|
var intent = new Intent(Settings.ActionAccessibilitySettings);
|
||||||
activity.StartActivity(intent);
|
activity.StartActivity(intent);
|
||||||
}
|
}
|
||||||
@@ -503,7 +537,7 @@ namespace Bit.Droid.Services
|
|||||||
|
|
||||||
public void OpenAutofillSettings()
|
public void OpenAutofillSettings()
|
||||||
{
|
{
|
||||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var intent = new Intent(Settings.ActionRequestSetAutofillService);
|
var intent = new Intent(Settings.ActionRequestSetAutofillService);
|
||||||
@@ -531,10 +565,92 @@ namespace Bit.Droid.Services
|
|||||||
// ref: https://developer.android.com/reference/android/os/SystemClock#elapsedRealtime()
|
// ref: https://developer.android.com/reference/android/os/SystemClock#elapsedRealtime()
|
||||||
return SystemClock.ElapsedRealtime();
|
return SystemClock.ElapsedRealtime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ExecuteFido2CredentialActionAsync(AppOptions appOptions)
|
||||||
|
{
|
||||||
|
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||||
|
if (activity == null || string.IsNullOrWhiteSpace(appOptions.Fido2CredentialAction))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appOptions.Fido2CredentialAction == CredentialProviderConstants.Fido2CredentialGet)
|
||||||
|
{
|
||||||
|
await ExecuteFido2GetCredentialAsync(appOptions);
|
||||||
|
}
|
||||||
|
else if (appOptions.Fido2CredentialAction == CredentialProviderConstants.Fido2CredentialCreate)
|
||||||
|
{
|
||||||
|
await ExecuteFido2CreateCredentialAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear CredentialAction and FromFido2Framework values to avoid erratic behaviors in subsequent navigation/flows
|
||||||
|
// For Fido2CredentialGet these are no longer needed as a new Activity will be initiated.
|
||||||
|
// For Fido2CredentialCreate the app will rely on IFido2MakeCredentialConfirmationUserInterface.IsConfirmingNewCredential
|
||||||
|
appOptions.Fido2CredentialAction = null;
|
||||||
|
appOptions.FromFido2Framework = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteFido2GetCredentialAsync(AppOptions appOptions)
|
||||||
|
{
|
||||||
|
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||||
|
if (activity == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = AndroidX.Credentials.Provider.PendingIntentHandler.RetrieveBeginGetCredentialRequest(activity.Intent);
|
||||||
|
var response = new AndroidX.Credentials.Provider.BeginGetCredentialResponse();;
|
||||||
|
var credentialEntries = new List<AndroidX.Credentials.Provider.CredentialEntry>();
|
||||||
|
foreach (var option in request.BeginGetCredentialOptions.OfType<AndroidX.Credentials.Provider.BeginGetPublicKeyCredentialOption>())
|
||||||
|
{
|
||||||
|
credentialEntries.AddRange(await Bit.App.Platforms.Android.Autofill.CredentialHelpers.PopulatePasskeyDataAsync(request.CallingAppInfo, option, activity, appOptions.HasUnlockedInThisTransaction));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentialEntries.Any())
|
||||||
|
{
|
||||||
|
response = new AndroidX.Credentials.Provider.BeginGetCredentialResponse.Builder()
|
||||||
|
.SetCredentialEntries(credentialEntries)
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new Android.Content.Intent();
|
||||||
|
AndroidX.Credentials.Provider.PendingIntentHandler.SetBeginGetCredentialResponse(result, response);
|
||||||
|
activity.SetResult(Result.Ok, result);
|
||||||
|
activity.Finish();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Bit.Core.Services.LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
|
||||||
|
activity.SetResult(Result.Canceled);
|
||||||
|
activity.Finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteFido2CreateCredentialAsync()
|
||||||
|
{
|
||||||
|
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||||
|
if (activity == null) { return; }
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var getRequest = AndroidX.Credentials.Provider.PendingIntentHandler.RetrieveProviderCreateCredentialRequest(activity.Intent);
|
||||||
|
await Bit.App.Platforms.Android.Autofill.CredentialHelpers.CreateCipherPasskeyAsync(getRequest, activity);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Bit.Core.Services.LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
|
||||||
|
activity.SetResult(Result.Canceled);
|
||||||
|
activity.Finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void CloseMainApp()
|
public void CloseMainApp()
|
||||||
{
|
{
|
||||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||||
if (activity == null)
|
if (activity == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -548,6 +664,8 @@ namespace Bit.Droid.Services
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool SupportsCredentialProviderService() => Build.VERSION.SdkInt >= BuildVersionCodes.UpsideDownCake;
|
||||||
|
|
||||||
public bool SupportsAutofillServices() => Build.VERSION.SdkInt >= BuildVersionCodes.O;
|
public bool SupportsAutofillServices() => Build.VERSION.SdkInt >= BuildVersionCodes.O;
|
||||||
|
|
||||||
public bool SupportsInlineAutofill() => Build.VERSION.SdkInt >= BuildVersionCodes.R;
|
public bool SupportsInlineAutofill() => Build.VERSION.SdkInt >= BuildVersionCodes.R;
|
||||||
@@ -573,7 +691,7 @@ namespace Bit.Droid.Services
|
|||||||
|
|
||||||
public float GetSystemFontSizeScale()
|
public float GetSystemFontSizeScale()
|
||||||
{
|
{
|
||||||
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity as MainActivity;
|
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||||
return activity?.Resources?.Configuration?.FontScale ?? 1;
|
return activity?.Resources?.Configuration?.FontScale ?? 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using Bit.Core.Abstractions;
|
|||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Droid.Accessibility;
|
using Bit.Droid.Accessibility;
|
||||||
using Java.Lang;
|
using Java.Lang;
|
||||||
|
using Bit.App.Droid.Utilities;
|
||||||
|
|
||||||
namespace Bit.Droid.Tile
|
namespace Bit.Droid.Tile
|
||||||
{
|
{
|
||||||
@@ -76,7 +77,7 @@ namespace Bit.Droid.Tile
|
|||||||
var intent = new Intent(this, typeof(AccessibilityActivity));
|
var intent = new Intent(this, typeof(AccessibilityActivity));
|
||||||
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
|
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
|
||||||
intent.PutExtra("autofillTileClicked", true);
|
intent.PutExtra("autofillTileClicked", true);
|
||||||
StartActivityAndCollapse(intent);
|
this.StartActivityAndCollapseWithIntent(intent, isMutable: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowConfigErrorDialog()
|
private void ShowConfigErrorDialog()
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
using System;
|
using Android.App;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
using Android.App;
|
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.OS;
|
|
||||||
using Android.Runtime;
|
using Android.Runtime;
|
||||||
using Android.Service.QuickSettings;
|
using Android.Service.QuickSettings;
|
||||||
using Android.Views;
|
using Bit.App.Droid.Utilities;
|
||||||
using Android.Widget;
|
|
||||||
using Java.Lang;
|
using Java.Lang;
|
||||||
|
|
||||||
namespace Bit.Droid.Tile
|
namespace Bit.Droid.Tile
|
||||||
@@ -62,7 +55,7 @@ namespace Bit.Droid.Tile
|
|||||||
var intent = new Intent(this, typeof(MainActivity));
|
var intent = new Intent(this, typeof(MainActivity));
|
||||||
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
|
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
|
||||||
intent.PutExtra("generatorTile", true);
|
intent.PutExtra("generatorTile", true);
|
||||||
StartActivityAndCollapse(intent);
|
this.StartActivityAndCollapseWithIntent(intent, isMutable: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
using System;
|
using Android.App;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
using Android.App;
|
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.OS;
|
|
||||||
using Android.Runtime;
|
using Android.Runtime;
|
||||||
using Android.Service.QuickSettings;
|
using Android.Service.QuickSettings;
|
||||||
using Android.Views;
|
using Bit.App.Droid.Utilities;
|
||||||
using Android.Widget;
|
|
||||||
using Java.Lang;
|
using Java.Lang;
|
||||||
|
|
||||||
namespace Bit.Droid.Tile
|
namespace Bit.Droid.Tile
|
||||||
@@ -63,7 +56,7 @@ namespace Bit.Droid.Tile
|
|||||||
var intent = new Intent(this, typeof(MainActivity));
|
var intent = new Intent(this, typeof(MainActivity));
|
||||||
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
|
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
|
||||||
intent.PutExtra("myVaultTile", true);
|
intent.PutExtra("myVaultTile", true);
|
||||||
StartActivityAndCollapse(intent);
|
this.StartActivityAndCollapseWithIntent(intent, isMutable: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.OS;
|
using Android.OS;
|
||||||
using Android.Provider;
|
using Android.Provider;
|
||||||
|
using Android.Service.QuickSettings;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
|
|
||||||
namespace Bit.App.Droid.Utilities
|
namespace Bit.App.Droid.Utilities
|
||||||
@@ -64,5 +65,26 @@ namespace Bit.App.Droid.Utilities
|
|||||||
|
|
||||||
return pendingIntentFlags;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using Android.OS;
|
||||||
|
using AndroidX.Credentials.Provider;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Java.Security;
|
||||||
|
|
||||||
|
namespace Bit.App.Droid.Utilities
|
||||||
|
{
|
||||||
|
public static class CallingAppInfoExtensions
|
||||||
|
{
|
||||||
|
public static string GetAndroidOrigin(this CallingAppInfo callingAppInfo)
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SdkInt < BuildVersionCodes.P || callingAppInfo?.SigningInfo?.GetApkContentsSigners().Any() != true)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cert = callingAppInfo.SigningInfo.GetApkContentsSigners()[0].ToByteArray();
|
||||||
|
var md = MessageDigest.GetInstance("SHA-256");
|
||||||
|
var certHash = md.Digest(cert);
|
||||||
|
return $"android:apk-key-hash:{CoreHelpers.Base64UrlEncode(certHash)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetLatestCertificationFingerprint(this CallingAppInfo callingAppInfo)
|
||||||
|
{
|
||||||
|
if (callingAppInfo.SigningInfo.HasMultipleSigners)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var signature = callingAppInfo.SigningInfo.GetSigningCertificateHistory()[0].ToByteArray();
|
||||||
|
var md = MessageDigest.GetInstance("SHA-256");
|
||||||
|
var digestedSignature = md.Digest(signature);
|
||||||
|
var normalizedFingerprint = string.Join(":", digestedSignature.Select(b => b.ToString("X2")));
|
||||||
|
return normalizedFingerprint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ namespace Bit.iOS
|
|||||||
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
||||||
if (needsAutofillReplacement.GetValueOrDefault())
|
if (needsAutofillReplacement.GetValueOrDefault())
|
||||||
{
|
{
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (message.Command == "showAppExtension")
|
else if (message.Command == "showAppExtension")
|
||||||
@@ -102,7 +102,7 @@ namespace Bit.iOS
|
|||||||
var success = value as bool?;
|
var success = value as bool?;
|
||||||
if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12)
|
if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12)
|
||||||
{
|
{
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,22 +114,21 @@ namespace Bit.iOS
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await ASHelpers.IdentitiesCanIncremental())
|
if (await ASHelpers.IdentitiesSupportIncrementalAsync())
|
||||||
{
|
{
|
||||||
var cipherId = message.Data as string;
|
var cipherId = message.Data as string;
|
||||||
if (message.Command == "addedCipher" && !string.IsNullOrWhiteSpace(cipherId))
|
if (message.Command == "addedCipher" && !string.IsNullOrWhiteSpace(cipherId))
|
||||||
{
|
{
|
||||||
var identity = await ASHelpers.GetCipherIdentityAsync(cipherId);
|
var identity = await ASHelpers.GetCipherPasswordIdentityAsync(cipherId);
|
||||||
if (identity == null)
|
if (identity == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await ASCredentialIdentityStore.SharedStore?.SaveCredentialIdentitiesAsync(
|
await ASCredentialIdentityStoreExtensions.SaveCredentialIdentitiesAsync(identity);
|
||||||
new ASPasswordCredentialIdentity[] { identity });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher")
|
else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher")
|
||||||
{
|
{
|
||||||
@@ -138,28 +137,27 @@ namespace Bit.iOS
|
|||||||
return;
|
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);
|
message.Data as Bit.Core.Models.View.CipherView);
|
||||||
if (identity == null)
|
if (identity == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await ASCredentialIdentityStore.SharedStore?.RemoveCredentialIdentitiesAsync(
|
await ASCredentialIdentityStoreExtensions.RemoveCredentialIdentitiesAsync(identity);
|
||||||
new ASPasswordCredentialIdentity[] { identity });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
else if (message.Command == "logout" && UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
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")
|
else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher")
|
||||||
&& UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
&& UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||||
{
|
{
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND)
|
else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND)
|
||||||
{
|
{
|
||||||
@@ -168,12 +166,12 @@ namespace Bit.iOS
|
|||||||
{
|
{
|
||||||
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||||
{
|
{
|
||||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.8bit.bitwarden</string>
|
<string>com.8bit.bitwarden</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2024.2.2</string>
|
<string>2024.6.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>CFBundleIconName</key>
|
<key>CFBundleIconName</key>
|
||||||
|
|||||||
41
src/App/Platforms/iOS/PrivacyInfo.xcprivacy
Normal file
41
src/App/Platforms/iOS/PrivacyInfo.xcprivacy
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>C617.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>35F9.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>E174.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>1C8F.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
481
src/App/Resources/Raw/fido2_privileged_allow_list.json
Normal file
481
src/App/Resources/Raw/fido2_privileged_allow_list.json
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.android.chrome",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.chrome.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "DA:63:3D:34:B6:9E:63:AE:21:03:B4:9D:53:CE:05:2F:C5:F7:F3:C5:3A:AB:94:FD:C2:A2:08:BD:FD:14:24:9C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.chrome.dev",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "90:44:EE:5F:EE:4B:BC:5E:21:DD:44:66:54:31:C4:EB:1F:1F:71:A3:27:16:A0:BC:92:7B:CB:B3:92:33:CA:BF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.chrome.canary",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "20:19:DF:A1:FB:23:EF:BF:70:C5:BC:D1:44:3C:5B:EA:B0:4F:3F:2F:F4:36:6E:9A:C1:E3:45:76:39:A2:4C:FC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.chromium.chrome",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.google.android.apps.chrome",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.fennec_webauthndebug",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.firefox",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.firefox_beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.focus",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.fennec_aurora",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "BC:04:88:83:8D:06:F4:CA:6B:F3:23:86:DA:AB:0D:D8:EB:CF:3E:77:30:78:74:59:F6:2F:B3:CD:14:A1:BA:AA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.rocket",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "86:3A:46:F0:97:39:32:B7:D0:19:9B:54:91:12:74:1C:2D:27:31:AC:72:EA:11:B7:52:3A:A9:0A:11:BF:56:91"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.canary",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.dev",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.rolling",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.local",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.brave.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.brave.browser_beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.brave.browser_nightly",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "app.vanadium.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.vivaldi.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.vivaldi.browser.snapshot",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.vivaldi.browser.sopranos",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.citrix.Receiver",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "3D:D1:12:67:10:69:AB:36:4E:F9:BE:73:9A:B7:B5:EE:15:E1:CD:E9:D8:75:7B:1B:F0:64:F5:0C:55:68:9A:49"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "CE:B2:23:D7:77:09:F2:B6:BC:0B:3A:78:36:F5:A5:AF:4C:E1:D3:55:F4:A7:28:86:F7:9D:F8:0D:C9:D6:12:2E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AA:D0:D4:57:E6:33:C3:78:25:77:30:5B:C1:B2:D9:E3:81:41:C7:21:DF:0D:AA:6E:29:07:2F:C4:1D:34:F0:AB"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.android.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C9:00:9D:01:EB:F9:F5:D0:30:2B:C7:1B:2F:E9:AA:9A:47:A4:32:BB:A1:73:08:A3:11:1B:75:D7:B2:14:90:25"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.sec.android.app.sbrowser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.sec.android.app.sbrowser.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.google.android.gms",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "7C:E8:3C:1B:71:F3:D5:72:FE:D0:4C:8D:40:C5:CB:10:FF:75:E6:D8:7D:9D:F6:FB:D5:3F:04:68:C2:90:50:53"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "D2:2C:C5:00:29:9F:B2:28:73:A0:1A:01:0D:E1:C8:2F:BE:4D:06:11:19:B9:48:14:DD:30:1D:AB:50:CB:76:78"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.alpha",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.corp",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.canary",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.broteam",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,9 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Enums;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Request;
|
using Bit.Core.Models.Request;
|
||||||
using Bit.Core.Models.Response;
|
using Bit.Core.Models.Response;
|
||||||
@@ -46,6 +41,7 @@ namespace Bit.Core.Abstractions
|
|||||||
Task<CipherResponse> PutShareCipherAsync(string id, CipherShareRequest request);
|
Task<CipherResponse> PutShareCipherAsync(string id, CipherShareRequest request);
|
||||||
Task PutDeleteCipherAsync(string id);
|
Task PutDeleteCipherAsync(string id);
|
||||||
Task<CipherResponse> PutRestoreCipherAsync(string id);
|
Task<CipherResponse> PutRestoreCipherAsync(string id);
|
||||||
|
Task<bool> HasUnassignedCiphersAsync();
|
||||||
Task RefreshIdentityTokenAsync();
|
Task RefreshIdentityTokenAsync();
|
||||||
Task<SsoPrevalidateResponse> PreValidateSsoAsync(string identifier);
|
Task<SsoPrevalidateResponse> PreValidateSsoAsync(string identifier);
|
||||||
Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path,
|
Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path,
|
||||||
@@ -99,5 +95,6 @@ namespace Bit.Core.Abstractions
|
|||||||
Task<bool> GetDevicesExistenceByTypes(DeviceType[] deviceTypes);
|
Task<bool> GetDevicesExistenceByTypes(DeviceType[] deviceTypes);
|
||||||
Task<ConfigResponse> GetConfigsAsync();
|
Task<ConfigResponse> GetConfigsAsync();
|
||||||
Task<string> GetFastmailAccountIdAsync(string apiKey);
|
Task<string> GetFastmailAccountIdAsync(string apiKey);
|
||||||
|
Task<List<Utilities.DigitalAssetLinks.Statement>> GetDigitalAssetLinksForRpAsync(string rpId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/Core/Abstractions/IAssetLinksService.cs
Normal file
7
src/Core/Abstractions/IAssetLinksService.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Bit.Core.Services
|
||||||
|
{
|
||||||
|
public interface IAssetLinksService
|
||||||
|
{
|
||||||
|
Task<bool> ValidateAssetLinksAsync(string rpId, string packageName, string normalizedFingerprint);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ namespace Bit.Core.Abstractions
|
|||||||
{
|
{
|
||||||
public interface IAutofillHandler
|
public interface IAutofillHandler
|
||||||
{
|
{
|
||||||
|
bool CredentialProviderServiceEnabled();
|
||||||
bool AutofillServicesEnabled();
|
bool AutofillServicesEnabled();
|
||||||
bool SupportsAutofillService();
|
bool SupportsAutofillService();
|
||||||
void Autofill(CipherView cipher);
|
void Autofill(CipherView cipher);
|
||||||
@@ -11,6 +12,7 @@ namespace Bit.Core.Abstractions
|
|||||||
bool AutofillAccessibilityServiceRunning();
|
bool AutofillAccessibilityServiceRunning();
|
||||||
bool AutofillAccessibilityOverlayPermitted();
|
bool AutofillAccessibilityOverlayPermitted();
|
||||||
bool AutofillServiceEnabled();
|
bool AutofillServiceEnabled();
|
||||||
|
void DisableCredentialProviderService();
|
||||||
void DisableAutofillService();
|
void DisableAutofillService();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Enums;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
@@ -37,5 +34,8 @@ namespace Bit.Core.Abstractions
|
|||||||
Task<byte[]> DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId);
|
Task<byte[]> DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId);
|
||||||
Task SoftDeleteWithServerAsync(string id);
|
Task SoftDeleteWithServerAsync(string id);
|
||||||
Task RestoreWithServerAsync(string id);
|
Task RestoreWithServerAsync(string id);
|
||||||
|
Task<string> CreateNewLoginForPasskeyAsync(Fido2ConfirmNewCredentialParams newPasskeyParams);
|
||||||
|
Task CopyTotpCodeIfNeededAsync(CipherView cipher);
|
||||||
|
Task<bool> VerifyOrganizationHasUnassignedItemsAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
using System;
|
namespace Bit.Core.Abstractions
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Bit.Core.Abstractions
|
|
||||||
{
|
{
|
||||||
public enum AwaiterPrecondition
|
public enum AwaiterPrecondition
|
||||||
{
|
{
|
||||||
EnvironmentUrlsInited
|
EnvironmentUrlsInited,
|
||||||
|
AndroidWindowCreated,
|
||||||
|
AutofillIOSExtensionViewDidAppear
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IConditionedAwaiterManager
|
public interface IConditionedAwaiterManager
|
||||||
@@ -13,5 +12,6 @@ namespace Bit.Core.Abstractions
|
|||||||
Task GetAwaiterForPrecondition(AwaiterPrecondition awaiterPrecondition);
|
Task GetAwaiterForPrecondition(AwaiterPrecondition awaiterPrecondition);
|
||||||
void SetAsCompleted(AwaiterPrecondition awaiterPrecondition);
|
void SetAsCompleted(AwaiterPrecondition awaiterPrecondition);
|
||||||
void SetException(AwaiterPrecondition awaiterPrecondition, Exception ex);
|
void SetException(AwaiterPrecondition awaiterPrecondition, Exception ex);
|
||||||
|
void Recreate(AwaiterPrecondition awaiterPrecondition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Domain;
|
||||||
|
|
||||||
namespace Bit.Core.Abstractions
|
namespace Bit.Core.Abstractions
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Models;
|
||||||
using Bit.App.Utilities.Prompts;
|
using Bit.App.Utilities.Prompts;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models;
|
using Bit.Core.Models;
|
||||||
@@ -28,6 +29,7 @@ namespace Bit.App.Abstractions
|
|||||||
bool SupportsNfc();
|
bool SupportsNfc();
|
||||||
bool SupportsCamera();
|
bool SupportsCamera();
|
||||||
bool SupportsFido2();
|
bool SupportsFido2();
|
||||||
|
bool SupportsCredentialProviderService();
|
||||||
bool SupportsAutofillServices();
|
bool SupportsAutofillServices();
|
||||||
bool SupportsInlineAutofill();
|
bool SupportsInlineAutofill();
|
||||||
bool SupportsDrawOver();
|
bool SupportsDrawOver();
|
||||||
@@ -36,8 +38,10 @@ namespace Bit.App.Abstractions
|
|||||||
void RateApp();
|
void RateApp();
|
||||||
void OpenAccessibilitySettings();
|
void OpenAccessibilitySettings();
|
||||||
void OpenAccessibilityOverlayPermissionSettings();
|
void OpenAccessibilityOverlayPermissionSettings();
|
||||||
|
void OpenCredentialProviderSettings();
|
||||||
void OpenAutofillSettings();
|
void OpenAutofillSettings();
|
||||||
long GetActiveTime();
|
long GetActiveTime();
|
||||||
|
Task ExecuteFido2CredentialActionAsync(AppOptions appOptions);
|
||||||
void CloseMainApp();
|
void CloseMainApp();
|
||||||
float GetSystemFontSizeScale();
|
float GetSystemFontSizeScale();
|
||||||
Task OnAccountSwitchCompleteAsync();
|
Task OnAccountSwitchCompleteAsync();
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/Core/Abstractions/IFido2ClientService.cs
Normal file
37
src/Core/Abstractions/IFido2ClientService.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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>
|
||||||
|
/// <param name="extraParams">Extra parameters for the credential creation operation</param>
|
||||||
|
/// <returns>The new credential</returns>
|
||||||
|
Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, Fido2ExtraCreateCredentialParams extraParams);
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
/// <param name="extraParams">Extra parameters for the credential assertion operation</param>
|
||||||
|
/// <returns>The asserted credential</returns>
|
||||||
|
Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, Fido2ExtraAssertCredentialParams extraParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
|
namespace Bit.Core.Abstractions
|
||||||
|
{
|
||||||
|
public interface IFido2MakeCredentialConfirmationUserInterface : IFido2MakeCredentialUserInterface
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Call this method after the user chose where to save the new Fido2 credential.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cipherId">
|
||||||
|
/// Cipher ID where to save the new credential.
|
||||||
|
/// If <c>null</c> a new default passkey cipher item will be created
|
||||||
|
/// </param>
|
||||||
|
/// <param name="userVerified">
|
||||||
|
/// Whether the user has been verified or not.
|
||||||
|
/// If <c>null</c> verification has not taken place yet.
|
||||||
|
/// </param>
|
||||||
|
void Confirm(string cipherId, bool? userVerified);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Call this method after the user chose where to save the new Fido2 credential.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cipherId">
|
||||||
|
/// Cipher ID where to save the new credential.
|
||||||
|
/// If <c>null</c> a new default passkey cipher item will be created
|
||||||
|
/// </param>
|
||||||
|
/// <param name="alreadyHasFido2Credential">
|
||||||
|
/// If the cipher corresponding to the <paramref name="cipherId"/> already has a Fido2 credential.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="userVerified">
|
||||||
|
/// Whether the user has been verified or not.
|
||||||
|
/// If <c>null</c> verification has not taken place yet.
|
||||||
|
/// </param>
|
||||||
|
Task ConfirmAsync(string cipherId, bool alreadyHasFido2Credential, bool? userVerified);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cancels the current flow to make a credential
|
||||||
|
/// </summary>
|
||||||
|
void Cancel();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Call this if an exception needs to happen on the credential making process
|
||||||
|
/// </summary>
|
||||||
|
void OnConfirmationException(Exception ex);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if we are already confirming a new credential.
|
||||||
|
/// </summary>
|
||||||
|
bool IsConfirmingNewCredential { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Call this after the vault was unlocked so that Fido2 credential creation can proceed.
|
||||||
|
/// </summary>
|
||||||
|
void ConfirmVaultUnlocked();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if we are waiting for the vault to be unlocked.
|
||||||
|
/// </summary>
|
||||||
|
bool IsWaitingUnlockVault { get; }
|
||||||
|
|
||||||
|
Fido2UserVerificationOptions? GetCurrentUserVerificationOptions();
|
||||||
|
|
||||||
|
void SetCheckHasVaultBeenUnlockedInThisTransaction(Func<bool> checkHasVaultBeenUnlockedInThisTransaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
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, Fido2ExtraCreateCredentialParams extraParams);
|
||||||
|
Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, Fido2ExtraAssertCredentialParams extraParams);
|
||||||
|
|
||||||
|
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
|
namespace Bit.App.Abstractions
|
||||||
{
|
{
|
||||||
@@ -10,5 +9,7 @@ namespace Bit.App.Abstractions
|
|||||||
Task<bool> PromptAndCheckPasswordIfNeededAsync(CipherRepromptType repromptType = CipherRepromptType.Password);
|
Task<bool> PromptAndCheckPasswordIfNeededAsync(CipherRepromptType repromptType = CipherRepromptType.Password);
|
||||||
|
|
||||||
Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
|
Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
|
||||||
|
|
||||||
|
Task<bool> ShouldByPassMasterPasswordRepromptAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Enums;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
|
|
||||||
namespace Bit.Core.Abstractions
|
namespace Bit.Core.Abstractions
|
||||||
{
|
{
|
||||||
@@ -29,7 +26,7 @@ namespace Bit.Core.Abstractions
|
|||||||
bool SupportsDuo();
|
bool SupportsDuo();
|
||||||
Task<bool> SupportsBiometricAsync();
|
Task<bool> SupportsBiometricAsync();
|
||||||
Task<bool> IsBiometricIntegrityValidAsync(string bioIntegritySrcKey = null);
|
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();
|
long GetActiveTime();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,6 +186,9 @@ namespace Bit.Core.Abstractions
|
|||||||
Task<BwRegion?> GetActiveUserRegionAsync();
|
Task<BwRegion?> GetActiveUserRegionAsync();
|
||||||
Task<BwRegion?> GetPreAuthRegionAsync();
|
Task<BwRegion?> GetPreAuthRegionAsync();
|
||||||
Task SetPreAuthRegionAsync(BwRegion value);
|
Task SetPreAuthRegionAsync(BwRegion value);
|
||||||
|
Task ReloadStateAsync();
|
||||||
|
Task<bool> GetShouldCheckOrganizationUnassignedItemsAsync(string userId = null);
|
||||||
|
Task SetShouldCheckOrganizationUnassignedItemsAsync(bool shouldCheck, string userId = null);
|
||||||
[Obsolete("Use GetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")]
|
[Obsolete("Use GetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")]
|
||||||
Task<string> GetPinProtectedAsync(string userId = null);
|
Task<string> GetPinProtectedAsync(string userId = null);
|
||||||
[Obsolete("Use SetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")]
|
[Obsolete("Use SetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")]
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
using System.Threading.Tasks;
|
using Bit.Core.Services;
|
||||||
|
|
||||||
namespace Bit.Core.Abstractions
|
namespace Bit.Core.Abstractions
|
||||||
{
|
{
|
||||||
public interface IUserPinService
|
public interface IUserPinService
|
||||||
{
|
{
|
||||||
|
Task<bool> IsPinLockEnabledAsync();
|
||||||
Task SetupPinAsync(string pin, bool requireMasterPasswordOnRestart);
|
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
|
namespace Bit.Core.Abstractions
|
||||||
{
|
{
|
||||||
public interface IUserVerificationService
|
public interface IUserVerificationService
|
||||||
{
|
{
|
||||||
Task<bool> VerifyUser(string secret, VerificationType verificationType);
|
Task<bool> VerifyUser(string secret, VerificationType verificationType);
|
||||||
|
Task<bool> VerifyMasterPasswordAsync(string masterPassword);
|
||||||
Task<bool> HasMasterPasswordAsync(bool checkMasterKeyHash = false);
|
Task<bool> HasMasterPasswordAsync(bool checkMasterKeyHash = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ using Bit.Core;
|
|||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Models.Response;
|
using Bit.Core.Models.Response;
|
||||||
using Bit.Core.Pages;
|
using Bit.Core.Pages;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
|
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
|
||||||
namespace Bit.App
|
namespace Bit.App
|
||||||
@@ -36,6 +38,9 @@ namespace Bit.App
|
|||||||
private readonly IPushNotificationService _pushNotificationService;
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
private readonly IConfigService _configService;
|
private readonly IConfigService _configService;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
#if ANDROID
|
||||||
|
private LazyResolve<IFido2MakeCredentialConfirmationUserInterface> _fido2MakeCredentialConfirmationUserInterface = new LazyResolve<IFido2MakeCredentialConfirmationUserInterface>();
|
||||||
|
#endif
|
||||||
|
|
||||||
private static bool _isResumed;
|
private static bool _isResumed;
|
||||||
// these variables are static because the app is launching new activities on notification click, creating new instances of App.
|
// these variables are static because the app is launching new activities on notification click, creating new instances of App.
|
||||||
@@ -46,7 +51,6 @@ namespace Bit.App
|
|||||||
// This queue keeps those actions so that when the app has resumed they can still be executed.
|
// 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
|
// 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 readonly Queue<Action> _onResumeActions = new Queue<Action>();
|
||||||
private bool _hasNavigatedToAutofillWindow;
|
|
||||||
|
|
||||||
#if ANDROID
|
#if ANDROID
|
||||||
|
|
||||||
@@ -104,7 +108,10 @@ namespace Bit.App
|
|||||||
Options.MyVaultTile = appOptions.MyVaultTile;
|
Options.MyVaultTile = appOptions.MyVaultTile;
|
||||||
Options.GeneratorTile = appOptions.GeneratorTile;
|
Options.GeneratorTile = appOptions.GeneratorTile;
|
||||||
Options.FromAutofillFramework = appOptions.FromAutofillFramework;
|
Options.FromAutofillFramework = appOptions.FromAutofillFramework;
|
||||||
|
Options.FromFido2Framework = appOptions.FromFido2Framework;
|
||||||
|
Options.Fido2CredentialAction = appOptions.Fido2CredentialAction;
|
||||||
Options.CreateSend = appOptions.CreateSend;
|
Options.CreateSend = appOptions.CreateSend;
|
||||||
|
Options.HasUnlockedInThisTransaction = appOptions.HasUnlockedInThisTransaction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,41 +127,17 @@ namespace Bit.App
|
|||||||
return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally)
|
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
|
//When executing from CredentialProviderSelectionActivity we don't have "Options" so we need to filter "manually"
|
||||||
if (Options != null && (Options.FromAutofillFramework || Options.Uri != null || Options.OtpData != null || Options.CreateSend != null))
|
//In the CredentialProviderSelectionActivity we don't need to show any Page, so we just create a "dummy" Window with a NavigationPage to avoid crashing.
|
||||||
|
if (activationState != null
|
||||||
|
&& activationState.State.ContainsKey("CREDENTIAL_DATA")
|
||||||
|
&& activationState.State.ContainsKey("credentialProviderCipherId"))
|
||||||
{
|
{
|
||||||
_isResumed = true; //Specifically for the Autofill scenario we need to manually set the _isResumed here
|
return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally)
|
||||||
_hasNavigatedToAutofillWindow = true;
|
|
||||||
return new AutoFillWindow(new NavigationPage(new AndroidNavigationRedirectPage()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var homePage = new HomePage(Options);
|
_isResumed = true;
|
||||||
// WORKAROUND: If the user autofills with Accessibility Services enabled and goes back to the application then there is currently an issue
|
return new ResumeWindow(new NavigationPage(new AndroidNavigationRedirectPage(Options)));
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
//iOS doesn't use the CreateWindow override used in Android so we just set the Application.Current.MainPage directly
|
//iOS doesn't use the CreateWindow override used in Android so we just set the Application.Current.MainPage directly
|
||||||
@@ -171,7 +154,7 @@ namespace Bit.App
|
|||||||
Application.Current.MainPage = value;
|
Application.Current.MainPage = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public App() : this(null)
|
public App() : this(null)
|
||||||
@@ -201,132 +184,182 @@ namespace Bit.App
|
|||||||
|
|
||||||
_accountsManager.Init(() => Options, this);
|
_accountsManager.Init(() => Options, this);
|
||||||
|
|
||||||
|
_broadcasterService.Subscribe(nameof(App), BroadcastServiceMessageCallbackAsync);
|
||||||
|
|
||||||
Bootstrap();
|
Bootstrap();
|
||||||
_broadcasterService.Subscribe(nameof(App), async (message) =>
|
}
|
||||||
{
|
|
||||||
try
|
private async void BroadcastServiceMessageCallbackAsync(Message message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(message);
|
||||||
|
if (message.Command == "showDialog")
|
||||||
{
|
{
|
||||||
if (message.Command == "showDialog")
|
var details = message.Data as DialogDetails;
|
||||||
|
ArgumentNullException.ThrowIfNull(details);
|
||||||
|
|
||||||
|
var confirmed = true;
|
||||||
|
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
|
||||||
|
AppResources.Ok : details.ConfirmText;
|
||||||
|
await MainThread.InvokeOnMainThreadAsync(ShowDialogAction);
|
||||||
|
async Task ShowDialogAction()
|
||||||
{
|
{
|
||||||
var details = message.Data as DialogDetails;
|
if (!string.IsNullOrWhiteSpace(details.CancelText))
|
||||||
var confirmed = true;
|
|
||||||
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
|
|
||||||
AppResources.Ok : details.ConfirmText;
|
|
||||||
await MainThread.InvokeOnMainThreadAsync(async () =>
|
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(details.CancelText))
|
ArgumentNullException.ThrowIfNull(MainPage);
|
||||||
{
|
|
||||||
confirmed = await MainPage.DisplayAlert(details.Title, details.Text, confirmText,
|
confirmed = await MainPage.DisplayAlert(details.Title, details.Text, confirmText,
|
||||||
details.CancelText);
|
details.CancelText);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await MainPage.DisplayAlert(details.Title, details.Text, confirmText);
|
await _deviceActionService.DisplayAlertAsync(details.Title, details.Text, confirmText);
|
||||||
}
|
}
|
||||||
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
|
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#if IOS
|
#if IOS
|
||||||
else if (message.Command == AppHelpers.RESUMED_MESSAGE_COMMAND)
|
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)
|
||||||
{
|
{
|
||||||
ResumedAsync().FireAndForget();
|
Options.OtpData = new OtpData((string)message.Data);
|
||||||
}
|
}
|
||||||
else if (message.Command == "slept")
|
|
||||||
|
await MainThread.InvokeOnMainThreadAsync(ExecuteNavigationAction);
|
||||||
|
async Task ExecuteNavigationAction()
|
||||||
{
|
{
|
||||||
await SleptAsync();
|
if (MainPage is TabsPage tabsPage)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(tabsPage.Navigation);
|
||||||
|
ArgumentNullException.ThrowIfNull(tabsPage.Navigation.ModalStack);
|
||||||
|
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();
|
||||||
|
ArgumentNullException.ThrowIfNull(tabsPage.Navigation);
|
||||||
|
await tabsPage.Navigation.PushModalAsync(new NavigationPage(new CipherSelectionPage(Options)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (message.Command == Constants.CredentialNavigateToAutofillCipherMessageCommand && message.Data is Fido2ConfirmNewCredentialParams createParams)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(MainPage);
|
||||||
|
ArgumentNullException.ThrowIfNull(Options);
|
||||||
|
await MainThread.InvokeOnMainThreadAsync(NavigateToCipherSelectionPageAction);
|
||||||
|
void NavigateToCipherSelectionPageAction()
|
||||||
|
{
|
||||||
|
Options.Uri = createParams.RpId;
|
||||||
|
Options.SaveUsername = createParams.UserName;
|
||||||
|
Options.SaveName = createParams.CredentialName;
|
||||||
|
MainPage = new NavigationPage(new CipherSelectionPage(Options));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (message.Command == "convertAccountToKeyConnector")
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
#if ANDROID
|
||||||
|
if (message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED && _fido2MakeCredentialConfirmationUserInterface.Value.IsConfirmingNewCredential)
|
||||||
|
{
|
||||||
|
_fido2MakeCredentialConfirmationUserInterface.Value.OnConfirmationException(new AccountSwitchedException());
|
||||||
}
|
}
|
||||||
#endif
|
#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(async () =>
|
lock (_processingLoginRequestLock)
|
||||||
{
|
|
||||||
if (MainPage is TabsPage tabsPage)
|
|
||||||
{
|
|
||||||
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)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (message.Command == "convertAccountToKeyConnector")
|
|
||||||
{
|
{
|
||||||
await MainThread.InvokeOnMainThreadAsync(async () =>
|
// lock doesn't allow for async execution
|
||||||
{
|
CheckPasswordlessLoginRequestsAsync().Wait();
|
||||||
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 == Constants.NavigateToMessageCommand && message.Data is NavigationTarget navigationTarget)
|
||||||
{
|
{
|
||||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||||
|
{
|
||||||
|
Navigate(navigationTarget, null);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CheckPasswordlessLoginRequestsAsync()
|
private async Task CheckPasswordlessLoginRequestsAsync()
|
||||||
@@ -341,7 +374,6 @@ namespace Bit.App
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var notification = await _stateService.GetPasswordlessLoginNotificationAsync();
|
var notification = await _stateService.GetPasswordlessLoginNotificationAsync();
|
||||||
if (notification == null)
|
if (notification == null)
|
||||||
{
|
{
|
||||||
@@ -693,6 +725,15 @@ namespace Bit.App
|
|||||||
// If we are in background we add the Navigation Actions to a queue to execute when the app resumes.
|
// If we are in background we add the Navigation Actions to a queue to execute when the app resumes.
|
||||||
// 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
|
// 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
|
||||||
#if ANDROID
|
#if ANDROID
|
||||||
|
if (_fido2MakeCredentialConfirmationUserInterface != null && _fido2MakeCredentialConfirmationUserInterface.Value.IsConfirmingNewCredential)
|
||||||
|
{
|
||||||
|
// if it's creating passkey
|
||||||
|
// and we have an active pending TaskCompletionSource
|
||||||
|
// then we let the Fido2 Authenticator flow manage the navigation to avoid issues
|
||||||
|
// like duplicated navigation to lock page.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_isResumed)
|
if (!_isResumed)
|
||||||
{
|
{
|
||||||
_onResumeActions.Enqueue(() => NavigateImpl(navTarget, navParams));
|
_onResumeActions.Enqueue(() => NavigateImpl(navTarget, navParams));
|
||||||
|
|||||||
@@ -46,7 +46,11 @@ namespace Bit.Core
|
|||||||
public const string PreLoginEmailKey = "preLoginEmailKey";
|
public const string PreLoginEmailKey = "preLoginEmailKey";
|
||||||
public const string ConfigsKey = "configsKey";
|
public const string ConfigsKey = "configsKey";
|
||||||
public const string DisplayEuEnvironmentFlag = "display-eu-environment";
|
public const string DisplayEuEnvironmentFlag = "display-eu-environment";
|
||||||
|
public const string UnassignedItemsBannerFlag = "unassigned-items-banner";
|
||||||
public const string RegionEnvironment = "regionEnvironment";
|
public const string RegionEnvironment = "regionEnvironment";
|
||||||
|
public const string DuoCallback = "bitwarden://duo-callback";
|
||||||
|
public const string NavigateToMessageCommand = "navigateTo";
|
||||||
|
public const string CredentialNavigateToAutofillCipherMessageCommand = "credentialNavigateToAutofillCipher";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This key is used to store the value of "ShouldConnectToWatch" of the last user that had logged in
|
/// This key is used to store the value of "ShouldConnectToWatch" of the last user that had logged in
|
||||||
@@ -135,6 +139,7 @@ namespace Bit.Core
|
|||||||
public static string ShouldConnectToWatchKey(string userId) => $"shouldConnectToWatch_{userId}";
|
public static string ShouldConnectToWatchKey(string userId) => $"shouldConnectToWatch_{userId}";
|
||||||
public static string ScreenCaptureAllowedKey(string userId) => $"screenCaptureAllowed_{userId}";
|
public static string ScreenCaptureAllowedKey(string userId) => $"screenCaptureAllowed_{userId}";
|
||||||
public static string PendingAdminAuthRequest(string userId) => $"pendingAdminAuthRequest_{userId}";
|
public static string PendingAdminAuthRequest(string userId) => $"pendingAdminAuthRequest_{userId}";
|
||||||
|
public static string ShouldCheckOrganizationUnassignedItemsKey(string userId) => $"shouldCheckOrganizationUnassignedItems_{userId}";
|
||||||
[Obsolete]
|
[Obsolete]
|
||||||
public static string KeyKey(string userId) => $"key_{userId}";
|
public static string KeyKey(string userId) => $"key_{userId}";
|
||||||
[Obsolete]
|
[Obsolete]
|
||||||
|
|||||||
@@ -53,12 +53,13 @@ namespace Bit.App.Controls
|
|||||||
if (BindingContext is CipherItemViewModel cipherItemVM)
|
if (BindingContext is CipherItemViewModel cipherItemVM)
|
||||||
{
|
{
|
||||||
cipherItemVM.IconImageSuccesfullyLoaded = true;
|
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)
|
public void Icon_Error(object sender, FFImageLoading.Maui.CachedImageEvents.ErrorEventArgs e)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
HorizontalOptions="Center"
|
HorizontalOptions="Center"
|
||||||
VerticalOptions="Center"
|
VerticalOptions="Center"
|
||||||
StyleClass="list-icon, list-icon-platform"
|
StyleClass="list-icon, list-icon-platform"
|
||||||
Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}"
|
Text="{Binding ., Converter={StaticResource iconGlyphConverter}}"
|
||||||
ShouldUpdateFontSizeDynamicallyForAccesibility="True"
|
ShouldUpdateFontSizeDynamicallyForAccesibility="True"
|
||||||
AutomationProperties.IsInAccessibleTree="False"
|
AutomationProperties.IsInAccessibleTree="False"
|
||||||
AutomationId="CipherTypeIcon" />
|
AutomationId="CipherTypeIcon" />
|
||||||
|
|||||||
22
src/Core/Controls/Settings/ExternalLinkSubtitleItemView.xaml
Normal file
22
src/Core/Controls/Settings/ExternalLinkSubtitleItemView.xaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<controls:BaseSettingItemView
|
||||||
|
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
xmlns:core="clr-namespace:Bit.Core"
|
||||||
|
x:Class="Bit.App.Controls.ExternalLinkSubtitleItemView"
|
||||||
|
x:Name="_contentView"
|
||||||
|
ControlTemplate="{StaticResource SettingControlTemplate}">
|
||||||
|
<controls:BaseSettingItemView.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Tapped="ContentView_Tapped" />
|
||||||
|
</controls:BaseSettingItemView.GestureRecognizers>
|
||||||
|
|
||||||
|
<controls:IconLabel
|
||||||
|
Text="{Binding Source={x:Static core:BitwardenIcons.ExternalLink}}"
|
||||||
|
TextColor="{DynamicResource TextColor}"
|
||||||
|
FontSize="25"
|
||||||
|
Margin="6,0,7,0"
|
||||||
|
HorizontalOptions="End"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
SemanticProperties.Description="{Binding Title, Mode=OneWay, Source={x:Reference _contentView}}" />
|
||||||
|
</controls:BaseSettingItemView>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public partial class ExternalLinkSubtitleItemView : BaseSettingItemView
|
||||||
|
{
|
||||||
|
public static readonly BindableProperty GoToLinkCommandProperty = BindableProperty.Create(
|
||||||
|
nameof(GoToLinkCommand), typeof(ICommand), typeof(ExternalLinkSubtitleItemView));
|
||||||
|
|
||||||
|
public ExternalLinkSubtitleItemView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand GoToLinkCommand
|
||||||
|
{
|
||||||
|
get => GetValue(GoToLinkCommandProperty) as ICommand;
|
||||||
|
set => SetValue(GoToLinkCommandProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ContentView_Tapped(System.Object sender, System.EventArgs e)
|
||||||
|
{
|
||||||
|
GoToLinkCommand?.Execute(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
||||||
<PackageReference Include="LiteDB" Version="5.0.17" />
|
<PackageReference Include="LiteDB" Version="5.0.17" />
|
||||||
<PackageReference Include="PCLCrypto" Version="2.1.40-alpha" />
|
<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="zxcvbn-core" Version="7.0.92" />
|
||||||
<PackageReference Include="MessagePack.MSBuild.Tasks" Version="2.5.124">
|
<PackageReference Include="MessagePack.MSBuild.Tasks" Version="2.5.124">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
||||||
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.18" />
|
<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.Activity.Ktx" Version="1.7.2.1" />
|
||||||
|
<PackageReference Include="Xamarin.AndroidX.Credentials" Version="1.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' AND !$(DefineConstants.Contains(FDROID))">
|
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' AND !$(DefineConstants.Contains(FDROID))">
|
||||||
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet" Version="118.0.1.5" />
|
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet" Version="118.0.1.5" />
|
||||||
@@ -75,13 +77,14 @@
|
|||||||
<Folder Include="Utilities\Automation\" />
|
<Folder Include="Utilities\Automation\" />
|
||||||
<Folder Include="Utilities\Prompts\" />
|
<Folder Include="Utilities\Prompts\" />
|
||||||
<Folder Include="Resources\Localization\" />
|
<Folder Include="Resources\Localization\" />
|
||||||
|
<Folder Include="Utilities\Fido2\" />
|
||||||
<Folder Include="Controls\Picker\" />
|
<Folder Include="Controls\Picker\" />
|
||||||
<Folder Include="Controls\Avatar\" />
|
<Folder Include="Controls\Avatar\" />
|
||||||
|
<Folder Include="Services\UserVerification\" />
|
||||||
|
<Folder Include="Utilities\WebAuthenticatorMAUI\" />
|
||||||
|
<Folder Include="Resources\Images\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<MauiImage Include="Resources\Images\dotnet_bot.svg">
|
|
||||||
<BaseSize>168,208</BaseSize>
|
|
||||||
</MauiImage>
|
|
||||||
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
||||||
<MauiFont Include="Resources\Fonts\*" />
|
<MauiFont Include="Resources\Fonts\*" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -90,6 +93,9 @@
|
|||||||
<LastGenOutput>AppResources.Designer.cs</LastGenOutput>
|
<LastGenOutput>AppResources.Designer.cs</LastGenOutput>
|
||||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
|
<Compile Update="Controls\Settings\ExternalLinkSubtitleItemView.xaml.cs">
|
||||||
|
<DependentUpon>ExternalLinkSubtitleItemView.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
<Compile Update="Pages\AndroidNavigationRedirectPage.xaml.cs">
|
<Compile Update="Pages\AndroidNavigationRedirectPage.xaml.cs">
|
||||||
<DependentUpon>AndroidNavigationRedirectPage.xaml</DependentUpon>
|
<DependentUpon>AndroidNavigationRedirectPage.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
@@ -100,12 +106,25 @@
|
|||||||
</Compile>
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<MauiXaml Update="Controls\Settings\ExternalLinkSubtitleItemView.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</MauiXaml>
|
||||||
<MauiXaml Update="Pages\AndroidNavigationRedirectPage.xaml">
|
<MauiXaml Update="Pages\AndroidNavigationRedirectPage.xaml">
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
</MauiXaml>
|
</MauiXaml>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<None Remove="Utilities\Fido2\" />
|
||||||
<None Remove="Controls\Picker\" />
|
<None Remove="Controls\Picker\" />
|
||||||
<None Remove="Controls\Avatar\" />
|
<None Remove="Controls\Avatar\" />
|
||||||
|
<None Remove="Services\UserVerification\" />
|
||||||
|
<None Remove="Utilities\WebAuthenticatorMAUI\" />
|
||||||
|
<None Remove="Resources\Images\" />
|
||||||
|
<None Remove="Resources\Images\empty_items_state_dark.svg" />
|
||||||
|
<None Remove="Resources\Images\empty_items_state.svg" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
||||||
|
<MauiImage Include="Resources\Images\empty_items_state.svg" />
|
||||||
|
<MauiImage Include="Resources\Images\empty_items_state_dark.svg" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
10
src/Core/Exceptions/ValidationException.cs
Normal file
10
src/Core/Exceptions/ValidationException.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Bit.Core.Exceptions
|
||||||
|
{
|
||||||
|
public class ValidationException : Exception
|
||||||
|
{
|
||||||
|
public ValidationException(string localizedMessage)
|
||||||
|
: base(localizedMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Models.Domain;
|
|
||||||
|
|
||||||
namespace Bit.Core.Models.Api
|
namespace Bit.Core.Models.Api
|
||||||
{
|
{
|
||||||
@@ -21,6 +20,7 @@ namespace Bit.Core.Models.Api
|
|||||||
RpName = fido2Key.RpName?.EncryptedString;
|
RpName = fido2Key.RpName?.EncryptedString;
|
||||||
UserHandle = fido2Key.UserHandle?.EncryptedString;
|
UserHandle = fido2Key.UserHandle?.EncryptedString;
|
||||||
UserName = fido2Key.UserName?.EncryptedString;
|
UserName = fido2Key.UserName?.EncryptedString;
|
||||||
|
UserDisplayName = fido2Key.UserDisplayName?.EncryptedString;
|
||||||
Counter = fido2Key.Counter?.EncryptedString;
|
Counter = fido2Key.Counter?.EncryptedString;
|
||||||
CreationDate = fido2Key.CreationDate;
|
CreationDate = fido2Key.CreationDate;
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,7 @@ namespace Bit.Core.Models.Api
|
|||||||
public string RpName { get; set; }
|
public string RpName { get; set; }
|
||||||
public string UserHandle { get; set; }
|
public string UserHandle { get; set; }
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; }
|
||||||
|
public string UserDisplayName { get; set; }
|
||||||
public string Counter { get; set; }
|
public string Counter { get; set; }
|
||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.App.Models
|
namespace Bit.App.Models
|
||||||
@@ -9,6 +8,8 @@ namespace Bit.App.Models
|
|||||||
public bool MyVaultTile { get; set; }
|
public bool MyVaultTile { get; set; }
|
||||||
public bool GeneratorTile { get; set; }
|
public bool GeneratorTile { get; set; }
|
||||||
public bool FromAutofillFramework { get; set; }
|
public bool FromAutofillFramework { get; set; }
|
||||||
|
public bool FromFido2Framework { get; set; }
|
||||||
|
public string Fido2CredentialAction { get; set; }
|
||||||
public CipherType? FillType { get; set; }
|
public CipherType? FillType { get; set; }
|
||||||
public string Uri { get; set; }
|
public string Uri { get; set; }
|
||||||
public CipherType? SaveType { get; set; }
|
public CipherType? SaveType { get; set; }
|
||||||
@@ -25,6 +26,8 @@ namespace Bit.App.Models
|
|||||||
public bool CopyInsteadOfShareAfterSaving { get; set; }
|
public bool CopyInsteadOfShareAfterSaving { get; set; }
|
||||||
public bool HideAccountSwitcher { get; set; }
|
public bool HideAccountSwitcher { get; set; }
|
||||||
public OtpData? OtpData { get; set; }
|
public OtpData? OtpData { get; set; }
|
||||||
|
public bool HasUnlockedInThisTransaction { get; set; }
|
||||||
|
public bool HasJustLoggedInOrUnlocked { get; set; }
|
||||||
|
|
||||||
public void SetAllFrom(AppOptions o)
|
public void SetAllFrom(AppOptions o)
|
||||||
{
|
{
|
||||||
@@ -35,6 +38,7 @@ namespace Bit.App.Models
|
|||||||
MyVaultTile = o.MyVaultTile;
|
MyVaultTile = o.MyVaultTile;
|
||||||
GeneratorTile = o.GeneratorTile;
|
GeneratorTile = o.GeneratorTile;
|
||||||
FromAutofillFramework = o.FromAutofillFramework;
|
FromAutofillFramework = o.FromAutofillFramework;
|
||||||
|
Fido2CredentialAction = o.Fido2CredentialAction;
|
||||||
FillType = o.FillType;
|
FillType = o.FillType;
|
||||||
Uri = o.Uri;
|
Uri = o.Uri;
|
||||||
SaveType = o.SaveType;
|
SaveType = o.SaveType;
|
||||||
@@ -51,6 +55,7 @@ namespace Bit.App.Models
|
|||||||
CopyInsteadOfShareAfterSaving = o.CopyInsteadOfShareAfterSaving;
|
CopyInsteadOfShareAfterSaving = o.CopyInsteadOfShareAfterSaving;
|
||||||
HideAccountSwitcher = o.HideAccountSwitcher;
|
HideAccountSwitcher = o.HideAccountSwitcher;
|
||||||
OtpData = o.OtpData;
|
OtpData = o.OtpData;
|
||||||
|
HasUnlockedInThisTransaction = o.HasUnlockedInThisTransaction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace Bit.Core.Models.Data
|
|||||||
RpName = apiData.RpName;
|
RpName = apiData.RpName;
|
||||||
UserHandle = apiData.UserHandle;
|
UserHandle = apiData.UserHandle;
|
||||||
UserName = apiData.UserName;
|
UserName = apiData.UserName;
|
||||||
|
UserDisplayName = apiData.UserDisplayName;
|
||||||
Counter = apiData.Counter;
|
Counter = apiData.Counter;
|
||||||
CreationDate = apiData.CreationDate;
|
CreationDate = apiData.CreationDate;
|
||||||
}
|
}
|
||||||
@@ -33,6 +34,7 @@ namespace Bit.Core.Models.Data
|
|||||||
public string RpName { get; set; }
|
public string RpName { get; set; }
|
||||||
public string UserHandle { get; set; }
|
public string UserHandle { get; set; }
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; }
|
||||||
|
public string UserDisplayName { get; set; }
|
||||||
public string Counter { get; set; }
|
public string Counter { get; set; }
|
||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Models.Data;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.Core.Models.Data;
|
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Domain
|
namespace Bit.Core.Models.Domain
|
||||||
@@ -21,6 +17,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
nameof(RpName),
|
nameof(RpName),
|
||||||
nameof(UserHandle),
|
nameof(UserHandle),
|
||||||
nameof(UserName),
|
nameof(UserName),
|
||||||
|
nameof(UserDisplayName),
|
||||||
nameof(Counter)
|
nameof(Counter)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,6 +45,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
public EncString RpName { get; set; }
|
public EncString RpName { get; set; }
|
||||||
public EncString UserHandle { get; set; }
|
public EncString UserHandle { get; set; }
|
||||||
public EncString UserName { get; set; }
|
public EncString UserName { get; set; }
|
||||||
|
public EncString UserDisplayName { get; set; }
|
||||||
public EncString Counter { get; set; }
|
public EncString Counter { get; set; }
|
||||||
public DateTime CreationDate { 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
|
namespace Bit.Core.Models.Domain
|
||||||
{
|
{
|
||||||
@@ -9,7 +8,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
{
|
{
|
||||||
if (key == null)
|
if (key == null)
|
||||||
{
|
{
|
||||||
throw new Exception("Must provide key.");
|
throw new ArgumentKeyNullException(nameof(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (encType == null)
|
if (encType == null)
|
||||||
@@ -24,7 +23,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
}
|
}
|
||||||
else
|
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
|
else
|
||||||
{
|
{
|
||||||
throw new Exception("Unsupported encType/key length.");
|
throw new InvalidKeyOperationException("Unsupported encType/key length.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Key != null)
|
if (Key != null)
|
||||||
@@ -72,6 +71,32 @@ namespace Bit.Core.Models.Domain
|
|||||||
public string KeyB64 { get; set; }
|
public string KeyB64 { get; set; }
|
||||||
public string EncKeyB64 { get; set; }
|
public string EncKeyB64 { get; set; }
|
||||||
public string MacKeyB64 { 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
|
public class UserKey : SymmetricCryptoKey
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
using System;
|
using Bit.Core.Enums;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Resources.Localization;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.Models.View
|
namespace Bit.Core.Models.View
|
||||||
{
|
{
|
||||||
@@ -52,7 +51,7 @@ namespace Bit.Core.Models.View
|
|||||||
public DateTime? DeletedDate { get; set; }
|
public DateTime? DeletedDate { get; set; }
|
||||||
public CipherRepromptType Reprompt { get; set; }
|
public CipherRepromptType Reprompt { get; set; }
|
||||||
public CipherKey Key { get; set; }
|
public CipherKey Key { get; set; }
|
||||||
|
|
||||||
public ItemView Item
|
public ItemView Item
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -122,5 +121,14 @@ namespace Bit.Core.Models.View
|
|||||||
public bool IsClonable => OrganizationId is null;
|
public bool IsClonable => OrganizationId is null;
|
||||||
|
|
||||||
public bool HasFido2Credential => Type == CipherType.Login && Login?.HasFido2Credentials == true;
|
public bool HasFido2Credential => Type == CipherType.Login && Login?.HasFido2Credentials == true;
|
||||||
|
|
||||||
|
public string GetMainFido2CredentialUsername()
|
||||||
|
{
|
||||||
|
return Login?.MainFido2Credential?.UserName
|
||||||
|
.FallbackOnNullOrWhiteSpace(Login?.MainFido2Credential?.UserDisplayName)
|
||||||
|
.FallbackOnNullOrWhiteSpace(Login?.Username)
|
||||||
|
.FallbackOnNullOrWhiteSpace(Name)
|
||||||
|
.FallbackOnNullOrWhiteSpace(AppResources.UnknownAccount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System.Text.Json.Serialization;
|
||||||
using System.Collections.Generic;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.Models.View
|
namespace Bit.Core.Models.View
|
||||||
{
|
{
|
||||||
@@ -26,13 +26,42 @@ namespace Bit.Core.Models.View
|
|||||||
public string RpName { get; set; }
|
public string RpName { get; set; }
|
||||||
public string UserHandle { get; set; }
|
public string UserHandle { get; set; }
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; }
|
||||||
|
public string UserDisplayName { get; set; }
|
||||||
public string Counter { get; set; }
|
public string Counter { get; set; }
|
||||||
public DateTime CreationDate { 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 string SubTitle => UserName;
|
||||||
|
|
||||||
public override List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions => new List<KeyValuePair<string, LinkedIdType>>();
|
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);
|
public bool CanLaunch => !string.IsNullOrEmpty(RpId);
|
||||||
|
[JsonIgnore]
|
||||||
public string LaunchUri => $"https://{RpId}";
|
public string LaunchUri => $"https://{RpId}";
|
||||||
|
|
||||||
public bool IsUniqueAgainst(Fido2CredentialView fido2View) => fido2View?.RpId != RpId || fido2View?.UserName != UserName;
|
public bool IsUniqueAgainst(Fido2CredentialView fido2View) => fido2View?.RpId != RpId || fido2View?.UserName != UserName;
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Enums;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
|
|
||||||
namespace Bit.Core.Models.View
|
namespace Bit.Core.Models.View
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Resources.Localization;
|
using Bit.Core.Resources.Localization;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.Maui.Platform;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
@@ -26,7 +27,7 @@ namespace Bit.App.Pages
|
|||||||
_apiEntry.ReturnCommand = new Command(() => _identityEntry.Focus());
|
_apiEntry.ReturnCommand = new Command(() => _identityEntry.Focus());
|
||||||
_identityEntry.ReturnType = ReturnType.Next;
|
_identityEntry.ReturnType = ReturnType.Next;
|
||||||
_identityEntry.ReturnCommand = new Command(() => _iconsEntry.Focus());
|
_identityEntry.ReturnCommand = new Command(() => _iconsEntry.Focus());
|
||||||
_vm.SubmitSuccessAction = () => MainThread.BeginInvokeOnMainThread(async () => await SubmitSuccessAsync());
|
_vm.SubmitSuccessTask = () => MainThread.InvokeOnMainThreadAsync(SubmitSuccessAsync);
|
||||||
_vm.CloseAction = async () =>
|
_vm.CloseAction = async () =>
|
||||||
{
|
{
|
||||||
await Navigation.PopModalAsync();
|
await Navigation.PopModalAsync();
|
||||||
@@ -37,6 +38,12 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);
|
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);
|
||||||
await Navigation.PopModalAsync();
|
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)
|
private void Close_Clicked(object sender, EventArgs e)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ namespace Bit.App.Pages
|
|||||||
public string WebVaultUrl { get; set; }
|
public string WebVaultUrl { get; set; }
|
||||||
public string IconsUrl { get; set; }
|
public string IconsUrl { get; set; }
|
||||||
public string NotificationsUrls { get; set; }
|
public string NotificationsUrls { get; set; }
|
||||||
public Action SubmitSuccessAction { get; set; }
|
public Func<Task> SubmitSuccessTask { get; set; }
|
||||||
public Action CloseAction { get; set; }
|
public Action CloseAction { get; set; }
|
||||||
|
|
||||||
public async Task SubmitAsync()
|
public async Task SubmitAsync()
|
||||||
@@ -73,7 +73,10 @@ namespace Bit.App.Pages
|
|||||||
IconsUrl = resUrls.Icons;
|
IconsUrl = resUrls.Icons;
|
||||||
NotificationsUrls = resUrls.Notifications;
|
NotificationsUrls = resUrls.Notifications;
|
||||||
|
|
||||||
SubmitSuccessAction?.Invoke();
|
if (SubmitSuccessTask != null)
|
||||||
|
{
|
||||||
|
await SubmitSuccessTask();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ValidateUrls()
|
public bool ValidateUrls()
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ namespace Bit.App.Pages
|
|||||||
private readonly HomeViewModel _vm;
|
private readonly HomeViewModel _vm;
|
||||||
private readonly AppOptions _appOptions;
|
private readonly AppOptions _appOptions;
|
||||||
private IBroadcasterService _broadcasterService;
|
private IBroadcasterService _broadcasterService;
|
||||||
|
private IConditionedAwaiterManager _conditionedAwaiterManager;
|
||||||
|
|
||||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||||
|
|
||||||
public HomePage(AppOptions appOptions = null)
|
public HomePage(AppOptions appOptions = null)
|
||||||
{
|
{
|
||||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
|
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
|
||||||
|
_conditionedAwaiterManager = ServiceContainer.Resolve<IConditionedAwaiterManager>();
|
||||||
_appOptions = appOptions;
|
_appOptions = appOptions;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_vm = BindingContext as HomeViewModel;
|
_vm = BindingContext as HomeViewModel;
|
||||||
@@ -56,6 +58,8 @@ namespace Bit.App.Pages
|
|||||||
PerformNavigationOnAccountChangedOnLoad = false;
|
PerformNavigationOnAccountChangedOnLoad = false;
|
||||||
accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
|
accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_conditionedAwaiterManager.SetAsCompleted(AwaiterPrecondition.AndroidWindowCreated);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user