mirror of
https://github.com/bitwarden/mobile
synced 2025-12-11 05:43:30 +00:00
Compare commits
96 Commits
mobiletf/p
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b543459f8d | ||
|
|
d0609024df | ||
|
|
a798ae0761 | ||
|
|
7f8b19a0a7 | ||
|
|
e50175abe1 | ||
|
|
8f435b2f26 | ||
|
|
cdbdec8943 | ||
|
|
3d42da2a29 | ||
|
|
16303e581b | ||
|
|
bc4b03b994 | ||
|
|
484eb03eca | ||
|
|
d59f80776c | ||
|
|
44849cb0a2 | ||
|
|
65794c02b1 | ||
|
|
8bc90b9649 | ||
|
|
466bb81cb7 | ||
|
|
c957e46ff8 | ||
|
|
d2d442253b | ||
|
|
f1a2714e8a | ||
|
|
9083bb1ab8 | ||
|
|
a69420257d | ||
|
|
ef81365ae0 | ||
|
|
81ad6b3cc4 | ||
|
|
5ca0167dbb | ||
|
|
e1f3f3dfd9 | ||
|
|
53a5a9caaa | ||
|
|
94381eebf0 | ||
|
|
b43eaf4a0a | ||
|
|
b89d8214d2 | ||
|
|
902ce67867 | ||
|
|
062b15aae9 | ||
|
|
dcf441afd7 | ||
|
|
c987840936 | ||
|
|
6e93983711 | ||
|
|
70de8245fe | ||
|
|
77d9920aab | ||
|
|
b908f1c721 | ||
|
|
46a249de46 | ||
|
|
837628e9bc | ||
|
|
3262fdc9ec | ||
|
|
c763b0072b | ||
|
|
b81ce44cc7 | ||
|
|
8a43bb4655 | ||
|
|
44f8901519 | ||
|
|
463d837fa8 | ||
|
|
ad6ffad5d1 | ||
|
|
9edcc8b4f7 | ||
|
|
cf5c5aa114 | ||
|
|
36c7beb1e6 | ||
|
|
f6d798af94 | ||
|
|
08ed70d5b1 | ||
|
|
33e0187460 | ||
|
|
2b78859d06 | ||
|
|
81205154c4 | ||
|
|
14d2660b61 | ||
|
|
bad5673724 | ||
|
|
67b76a777e | ||
|
|
02f5936fce | ||
|
|
fc208b08d7 | ||
|
|
f165135147 | ||
|
|
d458f17ad6 | ||
|
|
c3bd4b84b1 | ||
|
|
0af78d0e03 | ||
|
|
e86a01a7db | ||
|
|
e16074a73e | ||
|
|
a333e72448 | ||
|
|
ffb7b3b8ac | ||
|
|
c8d0db9f31 | ||
|
|
8566f5c00a | ||
|
|
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 |
24
.github/CODEOWNERS
vendored
24
.github/CODEOWNERS
vendored
@@ -7,15 +7,6 @@
|
|||||||
# Default file owners
|
# Default file owners
|
||||||
* @bitwarden/dept-development-mobile
|
* @bitwarden/dept-development-mobile
|
||||||
|
|
||||||
# DevOps for Actions and other workflow changes
|
|
||||||
.github/workflows @bitwarden/dept-devops
|
|
||||||
|
|
||||||
# DevOps for Version Bumping
|
|
||||||
src/App/Platforms/Android/AndroidManifest.xml
|
|
||||||
src/iOS.Autofill/Info.plist
|
|
||||||
src/iOS.Extension/Info.plist
|
|
||||||
src/iOS.ShareExtension/Info.plist
|
|
||||||
src/App/Platforms/iOS/Info.plist
|
|
||||||
|
|
||||||
## Auth team files ##
|
## Auth team files ##
|
||||||
|
|
||||||
@@ -43,3 +34,18 @@ store/google/en
|
|||||||
|
|
||||||
## Utils ##
|
## Utils ##
|
||||||
store/google/Publisher
|
store/google/Publisher
|
||||||
|
|
||||||
|
## These workflows have joint ownership ##
|
||||||
|
.github/workflows/build.yml @bitwarden/dept-bre @bitwarden/dept-development-mobile
|
||||||
|
.github/workflows/build-beta.yml @bitwarden/dept-bre @bitwarden/dept-development-mobile
|
||||||
|
.github/workflows/cleanup-rc-branch.yml @bitwarden/dept-bre @bitwarden/dept-development-mobile
|
||||||
|
.github/workflows/release.yml @bitwarden/dept-bre @bitwarden/dept-development-mobile
|
||||||
|
.github/workflows/version-auto-bump.yml @bitwarden/dept-bre @bitwarden/dept-development-mobile
|
||||||
|
.github/workflows/version-bump.yml @bitwarden/dept-bre @bitwarden/dept-development-mobile
|
||||||
|
|
||||||
|
# Shared ownership for version bump automation
|
||||||
|
src/App/Platforms/Android/AndroidManifest.xml
|
||||||
|
src/iOS.Autofill/Info.plist
|
||||||
|
src/iOS.Extension/Info.plist
|
||||||
|
src/iOS.ShareExtension/Info.plist
|
||||||
|
src/App/Platforms/iOS/Info.plist
|
||||||
|
|||||||
18
.github/ISSUE_TEMPLATE/bug.yml
vendored
18
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -6,8 +6,20 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for taking the time to fill out this bug report!
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Testing the new Bitwarden Beta apps? Submit your report in [bitwarden/android](https://github.com/bitwarden/android) or [bitwarden/ios](https://github.com/bitwarden/ios)
|
||||||
|
|
||||||
|
|
||||||
Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.
|
Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.
|
||||||
|
- type: checkboxes
|
||||||
|
id: production
|
||||||
|
attributes:
|
||||||
|
label: Production Build
|
||||||
|
options:
|
||||||
|
- label: I'm using the legacy Bitwarden app pubicly available in App Store / Play Store and I'm aware that Bitwarden Beta bugs should be reported in [bitwarden/android](https://github.com/bitwarden/android) or [bitwarden/ios](https://github.com/bitwarden/ios)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: reproduce
|
id: reproduce
|
||||||
attributes:
|
attributes:
|
||||||
@@ -73,9 +85,3 @@ body:
|
|||||||
description: What version of our software are you running? (go to "Settings" → "About" in the app)
|
description: What version of our software are you running? (go to "Settings" → "About" in the app)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: checkboxes
|
|
||||||
id: beta
|
|
||||||
attributes:
|
|
||||||
label: Beta
|
|
||||||
options:
|
|
||||||
- label: Using a pre-release version of the application.
|
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,11 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
|
- name: Native Android Beta Bug Reports
|
||||||
|
url: https://github.com/bitwarden/android/issues
|
||||||
|
about: Bugs found in the new native Android Beta app should be reported in [bitwarden/android](https://github.com/bitwarden/android)
|
||||||
|
- name: Native iOS BETA Bug Reports
|
||||||
|
url: https://github.com/bitwarden/ios/issues
|
||||||
|
about: Bugs found in the new native iOS Beta app should be reported in [bitwarden/ios](https://github.com/bitwarden/ios)
|
||||||
- name: Customer Support
|
- name: Customer Support
|
||||||
url: https://bitwarden.com/contact/
|
url: https://bitwarden.com/contact/
|
||||||
about: Please contact our customer support for account issues and general customer support.
|
about: Please contact our customer support for account issues and general customer support.
|
||||||
|
|||||||
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/*
|
||||||
1
.github/renovate.json
vendored
1
.github/renovate.json
vendored
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"enabled": false,
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": [
|
||||||
"config:base",
|
"config:base",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: Automatic responses
|
name: Automatic responses
|
||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
@@ -7,7 +6,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:
|
||||||
|
|||||||
33
.github/workflows/build-beta.yml
vendored
33
.github/workflows/build-beta.yml
vendored
@@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: Build Beta
|
name: Build Beta
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -24,7 +23,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@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
with:
|
with:
|
||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
|
|
||||||
@@ -45,12 +44,12 @@ jobs:
|
|||||||
|
|
||||||
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
|
||||||
app_output_name: App
|
_APP_OUTPUT_NAME: App
|
||||||
app_ci_output_filename: App_x64_Debug
|
_APP_CI_OUTPUT_FILENAME: App_x64_Debug
|
||||||
steps:
|
steps:
|
||||||
- name: Set XCode version
|
- name: Set XCode version
|
||||||
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
|
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
|
||||||
@@ -58,12 +57,12 @@ 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@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
@@ -80,7 +79,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@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: ${{ inputs.ref }}
|
ref: ${{ inputs.ref }}
|
||||||
@@ -136,7 +135,7 @@ jobs:
|
|||||||
|
|
||||||
echo "### CFBundleVersion $BUILD_NUMBER" >> $GITHUB_STEP_SUMMARY
|
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
|
||||||
@@ -146,7 +145,7 @@ jobs:
|
|||||||
- name: Update Entitlements
|
- name: Update Entitlements
|
||||||
run: |
|
run: |
|
||||||
echo "##### Updating Entitlements"
|
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
|
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
|
- name: Get certificates
|
||||||
run: |
|
run: |
|
||||||
@@ -246,8 +245,8 @@ jobs:
|
|||||||
ARCHIVE_PATH: ./${{ env.main_app_folder_path }}/bin/Debug/${{ env.target-net-version }}-ios/iossimulator-x64
|
ARCHIVE_PATH: ./${{ env.main_app_folder_path }}/bin/Debug/${{ env.target-net-version }}-ios/iossimulator-x64
|
||||||
EXPORT_PATH: ./bitwarden-export
|
EXPORT_PATH: ./bitwarden-export
|
||||||
run: |
|
run: |
|
||||||
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: Show Bitwarden Export
|
- name: Show Bitwarden Export
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -266,7 +265,7 @@ jobs:
|
|||||||
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@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
with:
|
with:
|
||||||
name: Bitwarden iOS
|
name: Bitwarden iOS
|
||||||
path: |
|
path: |
|
||||||
@@ -275,10 +274,10 @@ 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@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
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
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Install AppCenter CLI
|
- name: Install AppCenter CLI
|
||||||
|
|||||||
197
.github/workflows/build.yml
vendored
197
.github/workflows/build.yml
vendored
@@ -1,19 +1,14 @@
|
|||||||
---
|
|
||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches-ignore:
|
|
||||||
- "l10n_master"
|
|
||||||
- "gh-pages"
|
|
||||||
paths-ignore:
|
|
||||||
- ".github/workflows/**"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
main_app_folder_path: src/App
|
main_app_folder_path: src/App
|
||||||
main_app_project_path: src/App/App.csproj
|
main_app_project_path: src/App/App.csproj
|
||||||
target-net-version: net8.0
|
target-net-version: net8.0
|
||||||
|
dotnet-version: '8.0.402'
|
||||||
|
maui-workload-version: '8.0.402'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cloc:
|
cloc:
|
||||||
@@ -21,7 +16,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@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
|
|
||||||
- name: Set up CLOC
|
- name: Set up CLOC
|
||||||
run: |
|
run: |
|
||||||
@@ -40,7 +35,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@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
with:
|
with:
|
||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
|
|
||||||
@@ -69,26 +64,30 @@ 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
|
_ANDROID_FOLDER_PATH_BASH: src/App/Platforms/Android
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- 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@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: ${{ env.dotnet-version }}
|
||||||
|
|
||||||
|
- name: Install MAUI Workload
|
||||||
|
run: |
|
||||||
|
dotnet workload install maui --version ${{ env.maui-workload-version }}
|
||||||
|
|
||||||
- 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
|
|
||||||
# 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: Setup Windows builder
|
- name: Setup Windows builder
|
||||||
run: choco install checksum --no-progress
|
run: choco install checksum --no-progress
|
||||||
@@ -108,11 +107,6 @@ jobs:
|
|||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- 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:
|
||||||
@@ -126,9 +120,9 @@ jobs:
|
|||||||
mkdir -p $HOME/secrets
|
mkdir -p $HOME/secrets
|
||||||
|
|
||||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
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
|
--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 \
|
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
|
--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 \
|
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||||
--name play_creds.json --file $HOME/secrets/play_creds.json --output none
|
--name play_creds.json --file $HOME/secrets/play_creds.json --output none
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -140,16 +134,16 @@ jobs:
|
|||||||
CONTAINER_NAME: mobile
|
CONTAINER_NAME: mobile
|
||||||
run: |
|
run: |
|
||||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||||
--name google-services.json --file ./${{ env.android_folder_path_bash }}/google-services.json --output none
|
--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=$((11000 + $GITHUB_RUN_NUMBER))
|
||||||
echo "##### Setting Android Version Code to $BUILD_NUMBER" | tee -a $GITHUB_STEP_SUMMARY
|
echo "##### Setting Android Version Code to $BUILD_NUMBER" | tee -a $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
sed -i "s/android:versionCode=\"1\"/android:versionCode=\"$BUILD_NUMBER\"/" \
|
sed -i "s/android:versionCode=\"1\"/android:versionCode=\"$BUILD_NUMBER\"/" \
|
||||||
./${{ env.android_folder_path_bash }}/AndroidManifest.xml
|
./${{ env._ANDROID_FOLDER_PATH_BASH }}/AndroidManifest.xml
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Restore packages
|
- name: Restore packages
|
||||||
@@ -193,7 +187,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
Write-Output "##### Sign Google Play Bundle Release Configuration"
|
Write-Output "##### Sign Google Play Bundle Release Configuration"
|
||||||
|
|
||||||
$signingUploadKeyStore = "$($env:GITHUB_WORKSPACE)\${{ env.android_folder_path }}\app_upload-keystore.jks"
|
$signingUploadKeyStore = "$($env:GITHUB_WORKSPACE)\${{ env._ANDROID_FOLDER_PATH }}\app_upload-keystore.jks"
|
||||||
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android `
|
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android `
|
||||||
/p:AndroidPackageFormats=aab `
|
/p:AndroidPackageFormats=aab `
|
||||||
/p:AndroidKeyStore=true `
|
/p:AndroidKeyStore=true `
|
||||||
@@ -210,7 +204,7 @@ jobs:
|
|||||||
|
|
||||||
Write-Output "##### Sign APK Release Configuration"
|
Write-Output "##### Sign APK Release Configuration"
|
||||||
|
|
||||||
$signingPlayKeyStore = "$($env:GITHUB_WORKSPACE)\${{ env.android_folder_path }}\app_play-keystore.jks"
|
$signingPlayKeyStore = "$($env:GITHUB_WORKSPACE)\${{ env._ANDROID_FOLDER_PATH }}\app_play-keystore.jks"
|
||||||
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android `
|
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android `
|
||||||
/p:AndroidKeyStore=true `
|
/p:AndroidKeyStore=true `
|
||||||
/p:AndroidSigningKeyStore=$signingPlayKeyStore `
|
/p:AndroidSigningKeyStore=$signingPlayKeyStore `
|
||||||
@@ -226,7 +220,7 @@ jobs:
|
|||||||
|
|
||||||
- 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@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
with:
|
with:
|
||||||
name: com.x8bit.bitwarden.aab
|
name: com.x8bit.bitwarden.aab
|
||||||
path: ./com.x8bit.bitwarden.aab
|
path: ./com.x8bit.bitwarden.aab
|
||||||
@@ -234,7 +228,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@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
with:
|
with:
|
||||||
name: com.x8bit.bitwarden.apk
|
name: com.x8bit.bitwarden.apk
|
||||||
path: ./com.x8bit.bitwarden.apk
|
path: ./com.x8bit.bitwarden.apk
|
||||||
@@ -242,7 +236,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@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
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 +256,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@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
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 +264,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@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
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
|
||||||
@@ -295,27 +289,31 @@ jobs:
|
|||||||
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_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: Checkout repo
|
||||||
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- 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@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: ${{ env.dotnet-version }}
|
||||||
|
|
||||||
|
- name: Install MAUI Workload
|
||||||
|
run: |
|
||||||
|
dotnet workload install maui --version ${{ env.maui-workload-version }}
|
||||||
|
|
||||||
- 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
|
|
||||||
# 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: Setup Windows builder
|
- name: Setup Windows builder
|
||||||
run: choco install checksum --no-progress
|
run: choco install checksum --no-progress
|
||||||
@@ -334,9 +332,6 @@ jobs:
|
|||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
|
||||||
|
|
||||||
- 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:
|
||||||
@@ -349,23 +344,23 @@ jobs:
|
|||||||
FILE: app_fdroid-keystore.jks
|
FILE: app_fdroid-keystore.jks
|
||||||
run: |
|
run: |
|
||||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME --name $FILE \
|
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME --name $FILE \
|
||||||
--file ${{ env.android_folder_path_bash }}/$FILE --output none
|
--file ${{ env._ANDROID_FOLDER_PATH_BASH }}/$FILE --output none
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Increment version
|
- name: Increment version
|
||||||
run: |
|
run: |
|
||||||
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
|
BUILD_NUMBER=$((11000 + $GITHUB_RUN_NUMBER))
|
||||||
echo "##### Setting F-Droid Version Code to $BUILD_NUMBER" | tee -a $GITHUB_STEP_SUMMARY
|
echo "##### Setting F-Droid Version Code to $BUILD_NUMBER" | tee -a $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
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 }}
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Clean for F-Droid
|
- name: Clean for F-Droid
|
||||||
run: |
|
run: |
|
||||||
$directoryBuildProps = $($env:GITHUB_WORKSPACE + "/Directory.Build.props");
|
$directoryBuildProps = $($env:GITHUB_WORKSPACE + "/Directory.Build.props");
|
||||||
|
|
||||||
$androidManifest = $($env:GITHUB_WORKSPACE + "/${{ env.android_manifest_path }}");
|
$androidManifest = $($env:GITHUB_WORKSPACE + "/${{ env._ANDROID_MANIFEST_PATH }}");
|
||||||
|
|
||||||
Write-Output "##### Back up project files"
|
Write-Output "##### Back up project files"
|
||||||
|
|
||||||
@@ -398,7 +393,7 @@ jobs:
|
|||||||
|
|
||||||
Write-Output "##### Sign FDroid"
|
Write-Output "##### Sign FDroid"
|
||||||
|
|
||||||
$signingFdroidKeyStore = "$($env:GITHUB_WORKSPACE)\${{ env.android_folder_path }}\app_fdroid-keystore.jks"
|
$signingFdroidKeyStore = "$($env:GITHUB_WORKSPACE)\${{ env._ANDROID_FOLDER_PATH }}\app_fdroid-keystore.jks"
|
||||||
dotnet build $projToBuild -c Release -f ${{ env.target-net-version }}-android `
|
dotnet build $projToBuild -c Release -f ${{ env.target-net-version }}-android `
|
||||||
/p:AndroidKeyStore=true `
|
/p:AndroidKeyStore=true `
|
||||||
/p:AndroidSigningKeyStore=$signingFdroidKeyStore `
|
/p:AndroidSigningKeyStore=$signingFdroidKeyStore `
|
||||||
@@ -414,7 +409,7 @@ jobs:
|
|||||||
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@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
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
|
||||||
@@ -426,7 +421,7 @@ 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@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
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
|
||||||
@@ -435,32 +430,35 @@ jobs:
|
|||||||
|
|
||||||
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
|
||||||
app_output_name: App
|
_APP_OUTPUT_NAME: App
|
||||||
app_ci_output_filename: App_x64_Debug
|
_APP_CI_OUTPUT_FILENAME: App_x64_Debug
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
|
with:
|
||||||
|
submodules: 'true'
|
||||||
|
|
||||||
- name: Set XCode version
|
- name: Set XCode version
|
||||||
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
|
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
|
||||||
with:
|
with:
|
||||||
xcode-version: 15.1
|
xcode-version: 15.4
|
||||||
|
|
||||||
- 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@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: ${{ env.dotnet-version }}
|
||||||
|
|
||||||
# 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
|
- name: Install MAUI Workload
|
||||||
run: dotnet workload install maui --ignore-failed-sources
|
run: dotnet workload install maui --version ${{ env.maui-workload-version }}
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@@ -469,11 +467,6 @@ jobs:
|
|||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
|
||||||
with:
|
|
||||||
submodules: 'true'
|
|
||||||
|
|
||||||
- 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:
|
||||||
@@ -519,10 +512,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Increment version
|
- name: Increment version
|
||||||
run: |
|
run: |
|
||||||
BUILD_NUMBER=$((100 + $GITHUB_RUN_NUMBER))
|
BUILD_NUMBER=$((8000 + $GITHUB_RUN_NUMBER))
|
||||||
echo "##### Setting iOS CFBundleVersion to $BUILD_NUMBER" | tee -a $GITHUB_STEP_SUMMARY
|
echo "##### Setting iOS CFBundleVersion to $BUILD_NUMBER" | tee -a $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./${{ env.ios_folder_path }}/Info.plist
|
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./${{ 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
|
||||||
@@ -532,7 +525,7 @@ jobs:
|
|||||||
- name: Update Entitlements
|
- name: Update Entitlements
|
||||||
run: |
|
run: |
|
||||||
echo "##### Updating Entitlements"
|
echo "##### Updating Entitlements"
|
||||||
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
|
- name: Get certificates
|
||||||
run: |
|
run: |
|
||||||
@@ -616,8 +609,8 @@ jobs:
|
|||||||
ARCHIVE_PATH: ./${{ env.main_app_folder_path }}/bin/Debug/${{ env.target-net-version }}-ios/iossimulator-x64
|
ARCHIVE_PATH: ./${{ env.main_app_folder_path }}/bin/Debug/${{ env.target-net-version }}-ios/iossimulator-x64
|
||||||
EXPORT_PATH: ./bitwarden-export
|
EXPORT_PATH: ./bitwarden-export
|
||||||
run: |
|
run: |
|
||||||
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:
|
env:
|
||||||
@@ -631,7 +624,7 @@ jobs:
|
|||||||
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@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
with:
|
with:
|
||||||
name: Bitwarden iOS
|
name: Bitwarden iOS
|
||||||
path: |
|
path: |
|
||||||
@@ -640,10 +633,10 @@ 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@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
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
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Install AppCenter CLI
|
- name: Install AppCenter CLI
|
||||||
@@ -677,19 +670,27 @@ jobs:
|
|||||||
echo "##### Uploading Watch dSYMs to Firebase"
|
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" \;
|
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: Set up private auth key
|
||||||
|
run: |
|
||||||
|
mkdir ~/private_keys
|
||||||
|
cat << EOF > ~/private_keys/AuthKey_U362LJ87AA.p8
|
||||||
|
${{ secrets.APP_STORE_CONNECT_AUTH_KEY }}
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Validate app in App Store
|
- name: Validate app in App Store
|
||||||
if: |
|
if: |
|
||||||
(github.ref == 'refs/heads/master'
|
(github.ref == 'refs/heads/main'
|
||||||
&& needs.setup.outputs.rc_branch_exists == 0
|
&& needs.setup.outputs.rc_branch_exists == 0
|
||||||
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
|| github.ref == 'refs/heads/hotfix-rc'
|
|| github.ref == 'refs/heads/hotfix-rc'
|
||||||
env:
|
|
||||||
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
|
||||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
|
||||||
run: |
|
run: |
|
||||||
xcrun altool --validate-app --type ios --file "./bitwarden-export/Bitwarden.ipa" \
|
xcrun altool \
|
||||||
--username "$APPLE_ID_USERNAME" --password "$APPLE_ID_PASSWORD"
|
--validate-app \
|
||||||
|
--type ios \
|
||||||
|
--file "./bitwarden-export/Bitwarden.ipa" \
|
||||||
|
--apiKey "U362LJ87AA" \
|
||||||
|
--apiIssuer ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }}
|
||||||
|
|
||||||
- name: Deploy to App Store
|
- name: Deploy to App Store
|
||||||
if: |
|
if: |
|
||||||
@@ -698,13 +699,13 @@ jobs:
|
|||||||
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
|| github.ref == 'refs/heads/hotfix-rc'
|
|| github.ref == 'refs/heads/hotfix-rc'
|
||||||
env:
|
|
||||||
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
|
||||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
|
||||||
run: |
|
run: |
|
||||||
xcrun altool --upload-app --type ios --file "./bitwarden-export/Bitwarden.ipa" \
|
xcrun altool \
|
||||||
--username "$APPLE_ID_USERNAME" --password "$APPLE_ID_PASSWORD"
|
--upload-app \
|
||||||
|
--type ios \
|
||||||
|
--file "./bitwarden-export/Bitwarden.ipa" \
|
||||||
|
--apiKey "U362LJ87AA" \
|
||||||
|
--apiIssuer ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }}
|
||||||
|
|
||||||
crowdin-push:
|
crowdin-push:
|
||||||
name: Crowdin Push
|
name: Crowdin Push
|
||||||
@@ -718,7 +719,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@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
|
|
||||||
- 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
|
||||||
@@ -733,7 +734,7 @@ jobs:
|
|||||||
secrets: "crowdin-api-token"
|
secrets: "crowdin-api-token"
|
||||||
|
|
||||||
- name: Upload Sources
|
- name: Upload Sources
|
||||||
uses: crowdin/github-action@67705afb6985401459cd143d5f5f00c9dc212f23 # v1.20.2
|
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 }}
|
||||||
|
|||||||
3
.github/workflows/cleanup-rc-branch.yml
vendored
3
.github/workflows/cleanup-rc-branch.yml
vendored
@@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: Cleanup RC Branch
|
name: Cleanup RC Branch
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -24,7 +23,7 @@ jobs:
|
|||||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
- name: Checkout main
|
- name: Checkout main
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
|
|||||||
20
.github/workflows/crowdin-pull.yml
vendored
20
.github/workflows/crowdin-pull.yml
vendored
@@ -1,21 +1,27 @@
|
|||||||
---
|
|
||||||
name: Crowdin Sync
|
name: Crowdin Sync
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs: {}
|
inputs: {}
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * 5'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
crowdin-sync:
|
crowdin-sync:
|
||||||
name: Autosync
|
name: Autosync
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-24.04
|
||||||
env:
|
env:
|
||||||
_CROWDIN_PROJECT_ID: "269690"
|
_CROWDIN_PROJECT_ID: "269690"
|
||||||
steps:
|
steps:
|
||||||
|
- name: Generate GH App token
|
||||||
|
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||||
|
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
|
||||||
- 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,9 +36,9 @@ 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@67705afb6985401459cd143d5f5f00c9dc212f23 # v1.20.2
|
uses: crowdin/github-action@61ac8b980551f674046220c3e104bddae2916ac5 # v2.0.0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ steps.app-token.outputs.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
|
||||||
|
|||||||
3
.github/workflows/enforce-labels.yml
vendored
3
.github/workflows/enforce-labels.yml
vendored
@@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: Enforce PR labels
|
name: Enforce PR labels
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -7,7 +6,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
|
||||||
|
|||||||
6
.github/workflows/pr-labeler.yml
vendored
6
.github/workflows/pr-labeler.yml
vendored
@@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: "Pull Request Labeler"
|
name: "Pull Request Labeler"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -10,8 +9,9 @@ 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@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
|
- name: Label PR
|
||||||
|
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
@@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: Release
|
name: Release
|
||||||
run-name: Release ${{ inputs.release_type }}
|
run-name: Release ${{ inputs.release_type }}
|
||||||
|
|
||||||
@@ -23,7 +22,7 @@ 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:
|
||||||
@@ -38,7 +37,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
|
|
||||||
- name: Check Release Version
|
- name: Check Release Version
|
||||||
id: version
|
id: version
|
||||||
@@ -65,25 +64,31 @@ 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: ${{ inputs.release_type != 'Dry Run' }}
|
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||||
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
|
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 }}
|
||||||
|
skip_unpack: true
|
||||||
|
|
||||||
- name: Dry Run - Download all artifacts
|
- name: Dry Run - Download all artifacts
|
||||||
if: ${{ inputs.release_type == 'Dry Run' }}
|
if: ${{ inputs.release_type == 'Dry Run' }}
|
||||||
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
|
uses: bitwarden/gh-actions/download-artifacts@main
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: main
|
branch: main
|
||||||
|
skip_unpack: true
|
||||||
|
|
||||||
- name: Prep Bitwarden iOS release asset
|
- name: Unzip release assets
|
||||||
run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS
|
run: |
|
||||||
|
unzip bw-android-apk-sha256.txt.zip -d bw-android-apk-sha256.txt
|
||||||
|
unzip bw-fdroid-apk-sha256.txt.zip -d bw-fdroid-apk-sha256.txt
|
||||||
|
unzip com.x8bit.bitwarden-fdroid.apk.zip -d com.x8bit.bitwarden-fdroid.apk
|
||||||
|
unzip com.x8bit.bitwarden.aab.zip -d com.x8bit.bitwarden.aab
|
||||||
|
unzip com.x8bit.bitwarden.apk.zip -d com.x8bit.bitwarden.apk
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||||
@@ -121,40 +126,36 @@ 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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
|
|
||||||
- name: Download F-Droid .apk artifact
|
- name: Download F-Droid .apk artifact
|
||||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||||
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
|
uses: bitwarden/gh-actions/download-artifacts@main
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: ${{ needs.release.outputs.branch-name }}
|
branch: ${{ needs.release.outputs.branch-name }}
|
||||||
name: com.x8bit.bitwarden-fdroid.apk
|
|
||||||
|
|
||||||
- name: Dry Run - Download F-Droid .apk artifact
|
- name: Dry Run - Download F-Droid .apk artifact
|
||||||
if: ${{ inputs.release_type == 'Dry Run' }}
|
if: ${{ inputs.release_type == 'Dry Run' }}
|
||||||
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
|
uses: bitwarden/gh-actions/download-artifacts@main
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: main
|
branch: main
|
||||||
name: com.x8bit.bitwarden-fdroid.apk
|
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
|
||||||
with:
|
with:
|
||||||
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,9 +168,10 @@ 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"
|
||||||
|
|
||||||
@@ -181,6 +183,28 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
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-mobile-fdroid"
|
||||||
|
|
||||||
|
- name: Import GPG key
|
||||||
|
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
|
||||||
|
with:
|
||||||
|
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
||||||
|
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
||||||
|
git_user_signingkey: true
|
||||||
|
git_commit_gpgsign: true
|
||||||
|
|
||||||
|
- name: 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: Download secrets
|
- name: Download secrets
|
||||||
env:
|
env:
|
||||||
ACCOUNT_NAME: bitwardenci
|
ACCOUNT_NAME: bitwardenci
|
||||||
@@ -194,28 +218,35 @@ jobs:
|
|||||||
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: ${{ inputs.release_type != 'Dry Run' }}
|
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||||
run: npm run deploy
|
env:
|
||||||
|
TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-mobile-fdroid }}
|
||||||
|
run: |
|
||||||
|
git remote set-url origin https://git:${TOKEN}@github.com/${GITHUB_REPOSITORY}.git
|
||||||
|
npm run deploy -- -u "bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>"
|
||||||
|
|||||||
3
.github/workflows/stale-bot.yml
vendored
3
.github/workflows/stale-bot.yml
vendored
@@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: 'Close stale issues and PRs'
|
name: 'Close stale issues and PRs'
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -8,7 +7,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
|
||||||
|
|||||||
1
.github/workflows/version-auto-bump.yml
vendored
1
.github/workflows/version-auto-bump.yml
vendored
@@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: Auto Bump Mobile Version
|
name: Auto Bump Mobile Version
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|||||||
25
.github/workflows/version-bump.yml
vendored
25
.github/workflows/version-bump.yml
vendored
@@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: Version Bump
|
name: Version Bump
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -12,6 +11,10 @@ on:
|
|||||||
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:
|
||||||
@@ -26,8 +29,16 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: ${{ inputs.version_number_override }}
|
version: ${{ inputs.version_number_override }}
|
||||||
|
|
||||||
|
- name: Slack Notification Check
|
||||||
|
run: |
|
||||||
|
if [[ "${{ inputs.enable_slack_notification }}" == true ]]; then
|
||||||
|
echo "Slack notifications enabled."
|
||||||
|
else
|
||||||
|
echo "Slack notifications disabled."
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Checkout Branch
|
- name: Checkout Branch
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
@@ -257,6 +268,14 @@ jobs:
|
|||||||
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
|
||||||
if: ${{ inputs.cut_rc_branch == true }}
|
if: ${{ inputs.cut_rc_branch == true }}
|
||||||
@@ -264,7 +283,7 @@ jobs:
|
|||||||
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@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
|
|||||||
17
.gitignore
vendored
17
.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.
|
||||||
@@ -295,17 +296,11 @@ iOSInjectionProject/
|
|||||||
timeline.xctimeline
|
timeline.xctimeline
|
||||||
playground.xcworkspace
|
playground.xcworkspace
|
||||||
|
|
||||||
# Swift Package Manager
|
# xcode / swift package manager - used by the MessagePack lib
|
||||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
/.build
|
||||||
# Packages/
|
/Packages
|
||||||
# Package.pins
|
/*.xcodeproj
|
||||||
# Package.resolved
|
.swiftpm
|
||||||
# *.xcodeproj
|
|
||||||
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
|
|
||||||
# hence it is not needed unless you have added a package configuration file to your project
|
|
||||||
# .swiftpm
|
|
||||||
|
|
||||||
.build/
|
|
||||||
|
|
||||||
# CocoaPods
|
# CocoaPods
|
||||||
# We recommend against adding the Pods directory to your .gitignore. However
|
# We recommend against adding the Pods directory to your .gitignore. However
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "lib/MessagePack"]
|
|
||||||
path = lib/MessagePack
|
|
||||||
url = https://github.com/bitwarden/MessagePack.git
|
|
||||||
@@ -4,9 +4,8 @@
|
|||||||
<ReleaseCodesignProvision>Automatic:AppStore</ReleaseCodesignProvision>
|
<ReleaseCodesignProvision>Automatic:AppStore</ReleaseCodesignProvision>
|
||||||
<ReleaseCodesignKey>iPhone Distribution</ReleaseCodesignKey>
|
<ReleaseCodesignKey>iPhone Distribution</ReleaseCodesignKey>
|
||||||
<IncludeBitwardeniOSExtensions>True</IncludeBitwardeniOSExtensions>
|
<IncludeBitwardeniOSExtensions>True</IncludeBitwardeniOSExtensions>
|
||||||
<IncludeBitwardenWatchOSApp>True</IncludeBitwardenWatchOSApp>
|
<IncludeBitwardenWatchOSApp>False</IncludeBitwardenWatchOSApp>
|
||||||
<Argon2IdLoadMtouchExtraArgs>-gcc_flags "-L$(ProjectDir)../../lib/ios -largon2 -force_load $(ProjectDir)../../lib/ios/libargon2.a"</Argon2IdLoadMtouchExtraArgs>
|
<Argon2IdLoadMtouchExtraArgs>-gcc_flags "-L$(ProjectDir)../../lib/ios -largon2 -force_load $(ProjectDir)../../lib/ios/libargon2.a"</Argon2IdLoadMtouchExtraArgs>
|
||||||
|
|
||||||
<!-- Uncomment this when Unit Testing-->
|
<!-- Uncomment this when Unit Testing-->
|
||||||
<!-- <CustomConstants>UT</CustomConstants> -->
|
<!-- <CustomConstants>UT</CustomConstants> -->
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
|
|
||||||
# Bitwarden Mobile Application
|
# Bitwarden Mobile Application
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Looking for the new native apps? Head on over to [bitwarden/android](https://github.com/bitwarden/android) and [bitwarden/ios](https://github.com/bitwarden/ios)
|
||||||
|
|
||||||
|
|
||||||
<a href="https://play.google.com/store/apps/details?id=com.x8bit.bitwarden" target="_blank"><img alt="Get it on Google Play" src="https://imgur.com/YQzmZi9.png" width="153" height="46"></a> <a href="https://mobileapp.bitwarden.com/fdroid/" target="_blank"><img alt="Get it on F-Droid" src="https://i.imgur.com/HDicnzz.png" width="154" height="46"></a> <a href="https://itunes.apple.com/us/app/bitwarden-free-password-manager/id1137397744?mt=8" target="_blank"><img src="https://imgur.com/GdGqPMY.png" width="135" height="40"></a>
|
<a href="https://play.google.com/store/apps/details?id=com.x8bit.bitwarden" target="_blank"><img alt="Get it on Google Play" src="https://imgur.com/YQzmZi9.png" width="153" height="46"></a> <a href="https://mobileapp.bitwarden.com/fdroid/" target="_blank"><img alt="Get it on F-Droid" src="https://i.imgur.com/HDicnzz.png" width="154" height="46"></a> <a href="https://itunes.apple.com/us/app/bitwarden-free-password-manager/id1137397744?mt=8" target="_blank"><img src="https://imgur.com/GdGqPMY.png" width="135" height="40"></a>
|
||||||
|
|
||||||
The Bitwarden mobile application is written in C# using .NET MAUI.
|
The Bitwarden mobile application is written in C# using .NET MAUI.
|
||||||
@@ -12,7 +16,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/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 [Legacy Contributing Documentation](https://github.com/bitwarden/mobile/tree/main/docs/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.
|
||||||
|
|
||||||
# We're Hiring!
|
# We're Hiring!
|
||||||
|
|
||||||
|
|||||||
79
docs/architecture/index.mdx
Normal file
79
docs/architecture/index.mdx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
# .NET MAUI (legacy)
|
||||||
|
|
||||||
|
:::warning Legacy
|
||||||
|
|
||||||
|
This represents the **legacy** mobile app architecture done in .NET MAUI.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
The mobile .NET MAUI clients are Android and iOS applications with extensions and watchOS. They are
|
||||||
|
all located at https://github.com/bitwarden/mobile.
|
||||||
|
|
||||||
|
Principal structure is a follows:
|
||||||
|
|
||||||
|
- `App`: Main .NET MAUI project that shares code between both platforms (Android & iOS). One can see
|
||||||
|
specific platform code under the `Platforms` folder.
|
||||||
|
- `Core`: Shared code having both logical and UI parts of the app. Several classes are a port from
|
||||||
|
the Web Clients to C#. Here one can find most of the UI and logic since it's shared between App
|
||||||
|
and the iOS extensions.
|
||||||
|
- `iOS.Core`: Shared code used by the main iOS app and its extensions
|
||||||
|
- `iOS.Autofill`: iOS extension that handles Autofill
|
||||||
|
- `iOS.Extensions`: iOS extension that handles Autofill from the bottom sheet extension
|
||||||
|
- `iOS.ShareExtension`: iOS extension that handles sharing files through Send
|
||||||
|
- `watchOS`: All the code specific to the watchOS platform
|
||||||
|
- `bitwarden`: Stub iOS app so that the watchOS app has a companion app on Xcode
|
||||||
|
- `bitwarden WatchKit App`: Main Watch app where we set assets.
|
||||||
|
- `bitwarden WatchKit Extension`: All the logic and presentation logic for the Watch app is here
|
||||||
|
|
||||||
|
## Dependencies diagram
|
||||||
|
|
||||||
|
Below is a simplified dependencies diagram of the mobile repository.
|
||||||
|
|
||||||
|
```kroki type=plantuml
|
||||||
|
@startuml
|
||||||
|
skinparam BackgroundColor transparent
|
||||||
|
skinparam componentStyle rectangle
|
||||||
|
skinparam linetype ortho
|
||||||
|
|
||||||
|
title Simplified Dependencies Diagram
|
||||||
|
|
||||||
|
component "Core"
|
||||||
|
component "App"
|
||||||
|
component "iOS.Core"
|
||||||
|
component "iOS.Autofill"
|
||||||
|
component "iOS.Extension"
|
||||||
|
component "iOS.ShareExtension"
|
||||||
|
component "watchOS" {
|
||||||
|
component "bitwarden"
|
||||||
|
component "bitwarden WatchKit App"
|
||||||
|
component "bitwarden WatchKit Extension"
|
||||||
|
}
|
||||||
|
|
||||||
|
[App] --> [Core]
|
||||||
|
|
||||||
|
[iOS.Core] --> [App]
|
||||||
|
|
||||||
|
[App] --> [iOS.Core]
|
||||||
|
[App] --> [iOS.Autofill]
|
||||||
|
[App] --> [iOS.Extension]
|
||||||
|
[App] --> [iOS.ShareExtension]
|
||||||
|
[App] --> [bitwarden WatchKit App]
|
||||||
|
|
||||||
|
[iOS.Autofill] --> [Core]
|
||||||
|
[iOS.Autofill] --> [iOS.Core]
|
||||||
|
|
||||||
|
[iOS.Extension] --> [Core]
|
||||||
|
[iOS.Extension] --> [iOS.Core]
|
||||||
|
|
||||||
|
[iOS.ShareExtension] --> [Core]
|
||||||
|
[iOS.ShareExtension] --> [iOS.Core]
|
||||||
|
|
||||||
|
[bitwarden] --> [bitwarden WatchKit App]
|
||||||
|
[bitwarden WatchKit App] --> [bitwarden WatchKit Extension]
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
26
docs/architecture/overview.md
Normal file
26
docs/architecture/overview.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
# Overview
|
||||||
|
|
||||||
|
:::warning Legacy
|
||||||
|
|
||||||
|
This represents the **legacy** mobile app overview architecture done in .NET MAUI.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
The overall architecture of the mobile applications is pretty similar to the
|
||||||
|
[web clients](../../clients/overview.md) one following a layered architecture:
|
||||||
|
|
||||||
|
- State
|
||||||
|
- Services
|
||||||
|
- Presentation
|
||||||
|
|
||||||
|
Even though the State and Services layers are pretty similar to the web ones the Presentation layer
|
||||||
|
differs:
|
||||||
|
|
||||||
|
## Presentation
|
||||||
|
|
||||||
|
The presentation layer is implemented using .NET MAUI for the mobile apps, except for the watchOS
|
||||||
|
one which uses SwiftUI [see ADR](../../adr/0017-watchOS-use-swift.md)
|
||||||
186
docs/architecture/watchOS.md
Normal file
186
docs/architecture/watchOS.md
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# watchOS
|
||||||
|
|
||||||
|
:::warning Legacy
|
||||||
|
|
||||||
|
This represents the **legacy** watchOS app architecture done in .NET MAUI.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Overall architecture
|
||||||
|
|
||||||
|
The watchOS application is organized as follows:
|
||||||
|
|
||||||
|
- `src/watchOS`: All the code specific to the watchOS platform
|
||||||
|
- `bitwarden`: Stub iOS app so that the watchOS app has a companion app on Xcode
|
||||||
|
- `bitwarden WatchKit App`: Main Watch app where we set assets.
|
||||||
|
- `bitwarden WatchKit Extension`: All the logic and presentation logic for the Watch app is here
|
||||||
|
|
||||||
|
So almost all the things related to the watch app will be in the **WatchKit Extension**, the
|
||||||
|
WatchKit App one will be only for assets and some configs.
|
||||||
|
|
||||||
|
Then in the Extension we have a layered architecture:
|
||||||
|
|
||||||
|
- State (it's a really simplified version of the iOS state)
|
||||||
|
- Persistence (here we use `CoreData` to interact with the Database)
|
||||||
|
- Services (totp generation, crypto services and business logic)
|
||||||
|
- Presentation (use `SwiftUI` for the UI with an MVVM pattern)
|
||||||
|
|
||||||
|
## Integration with iOS
|
||||||
|
|
||||||
|
The watchOS app is developed using `Xcode` and `Swift` and we need to integrate it to the .NET MAUI
|
||||||
|
iOS application.
|
||||||
|
|
||||||
|
For this, the `iOS.csproj` has been adapted taking a
|
||||||
|
[solution](https://github.com/xamarin/xamarin-macios/issues/10070#issuecomment-1033428823) provided
|
||||||
|
in the `Xamarin.Forms` GitHub repository and modified to our needs:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<PropertyGroup>
|
||||||
|
<WatchAppBuildPath Condition=" '$(Configuration)' == 'Debug' ">$(Home)/Library/Developer/Xcode/DerivedData/bitwarden-cbtqsueryycvflfzbsoteofskiyr/Build/Products</WatchAppBuildPath>
|
||||||
|
<WatchAppBuildPath Condition=" '$(Configuration)' != 'Debug' ">$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..'))/watchOS/bitwarden.xcarchive/Products/Applications/bitwarden.app/Watch</WatchAppBuildPath>
|
||||||
|
<WatchAppBundle>Bitwarden.app</WatchAppBundle>
|
||||||
|
<WatchAppConfiguration Condition=" '$(Platform)' == 'iPhoneSimulator' ">watchsimulator</WatchAppConfiguration>
|
||||||
|
<WatchAppConfiguration Condition=" '$(Platform)' == 'iPhone' ">watchos</WatchAppConfiguration>
|
||||||
|
<WatchAppBundleFullPath Condition=" '$(Configuration)' == 'Debug' ">$(WatchAppBuildPath)/$(Configuration)-$(WatchAppConfiguration)/$(WatchAppBundle)</WatchAppBundleFullPath>
|
||||||
|
<WatchAppBundleFullPath Condition=" '$(Configuration)' != 'Debug' ">$(WatchAppBuildPath)/$(WatchAppBundle)</WatchAppBundleFullPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' AND Exists('$(WatchAppBundleFullPath)') ">
|
||||||
|
<_ResolvedWatchAppReferences Include="$(WatchAppBundleFullPath)" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup Condition=" '$(Configuration)' != 'Debug' ">
|
||||||
|
<_ResolvedWatchAppReferences Include="$(WatchAppBundleFullPath)" />
|
||||||
|
</ItemGroup>
|
||||||
|
<PropertyGroup Condition=" '$(_ResolvedWatchAppReferences)' != '' ">
|
||||||
|
<CodesignExtraArgs>--deep</CodesignExtraArgs>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Target Name="PrintWatchAppBundleStatus" BeforeTargets="Build">
|
||||||
|
<Message Text="WatchAppBundleFullPath: '$(WatchAppBundleFullPath)' exists" Condition=" Exists('$(WatchAppBundleFullPath)') " />
|
||||||
|
<Message Text="WatchAppBundleFullPath: '$(WatchAppBundleFullPath)' does NOT exist" Condition=" !Exists('$(WatchAppBundleFullPath)') " />
|
||||||
|
</Target>
|
||||||
|
```
|
||||||
|
|
||||||
|
So on the `PropertyGroup` the `WatchAppBundleFullPath` is assembled together depending on the
|
||||||
|
Configuration and the Platform taking the output of the Xcode watchOS app build. Then there are some
|
||||||
|
`ItemGroup` to include the watch app depending on if it exists and the Configuration. The task
|
||||||
|
`_ResolvedWatchAppReferences` is the one responsible to peek into the `Bitwarden.app` built by Xcode
|
||||||
|
and if it finds a Watch app, it will just bundle it to the Xamarin iOS application. Finally, if the
|
||||||
|
Watch app is bundled, deep signing is enabled and the build path is printed.
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
|
||||||
|
As one can see in the csproj, to bundle the watchOS app into the iOS app one needs to target the
|
||||||
|
correct platform. So if one is going to use a device, target the device on Xcode to build the
|
||||||
|
watchOS app and after the build is done one can go to VS4M to build the iOS app (which will bundle
|
||||||
|
the watchOS one) and run it on the device.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Synchronization between iPhone and Watch
|
||||||
|
|
||||||
|
In order to sync data between the iPhone and the Watch apps the
|
||||||
|
[Watch Connectivity Framework](https://developer.apple.com/documentation/watchconnectivity) is used.
|
||||||
|
|
||||||
|
So there is a Watch Connectivity Manager on each side that is the interface used for the services on
|
||||||
|
each platform to communicate.
|
||||||
|
|
||||||
|
For the sync communication, mainly
|
||||||
|
[updateApplicationContext](https://developer.apple.com/documentation/watchconnectivity/wcsession/1615621-updateapplicationcontext)
|
||||||
|
is used given that it always have the latest data sent available, it's sent in the background and
|
||||||
|
the counterpart device doesn't necessarily needs to be in range (so it's cached until it can be
|
||||||
|
delivered). Additionally,
|
||||||
|
[sendMessage](https://developer.apple.com/documentation/watchconnectivity/wcsession/1615687-sendmessage)
|
||||||
|
is also used to signal the counterpart of some action to take quickly (like triggering a sync from
|
||||||
|
the Watch).
|
||||||
|
|
||||||
|
The `WatchDTO` is the object that is sent in the synchronization that has all the information for
|
||||||
|
the Watch.
|
||||||
|
|
||||||
|
```kroki type=plantuml
|
||||||
|
title= iOS part
|
||||||
|
@startuml
|
||||||
|
|
||||||
|
title iOS
|
||||||
|
|
||||||
|
participant C as "Caller"
|
||||||
|
participant BWDS as "BaseWatchDeviceService"
|
||||||
|
participant WDS as "WatchDeviceService"
|
||||||
|
participant WCSM as "WCSessionManager"
|
||||||
|
boundary WCF as "Watch Connectivity Framework"
|
||||||
|
|
||||||
|
group Sync
|
||||||
|
C->>BWDS: SyncDataToWatchAsync(...)
|
||||||
|
BWDS->BWDS: GetStateAsync(...)
|
||||||
|
BWDS->>WDS: SendDataToWatchAsync(...)
|
||||||
|
WDS->>WCSM: SendBackgroundHighPriorityMessage(...)
|
||||||
|
WCSM->>WCF: UpdateApplicationContext(...)
|
||||||
|
end
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
```kroki type=plantuml
|
||||||
|
title= iOS part
|
||||||
|
@startuml
|
||||||
|
|
||||||
|
title watchOS
|
||||||
|
|
||||||
|
boundary WCF as "Watch Connectivity Framework"
|
||||||
|
participant WCM as "WatchConnectivityManager"
|
||||||
|
participant SS as "StateService"
|
||||||
|
participant ES as "EnvironmentService"
|
||||||
|
participant CS as "CipherService"
|
||||||
|
participant WCS as "watchConnectivitySubject"
|
||||||
|
|
||||||
|
group Sync
|
||||||
|
WCF->>WCM: didReceiveApplicationContext(...)
|
||||||
|
WCM->>SS: update state
|
||||||
|
WCM->>ES: update environment
|
||||||
|
WCM->>CS: saveCiphers(...)
|
||||||
|
WCM->>WCS: fire notification change to subscribers
|
||||||
|
end
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
## States
|
||||||
|
|
||||||
|
The next ones are the states in which the Watch application can be at a given time:
|
||||||
|
|
||||||
|
- **Valid:** Everything it's ok and the user can see the vault ciphers with TOTP
|
||||||
|
- **Need Login:** The user needs to log in using the iPhone
|
||||||
|
- **Need Setup:** The user needs to set up an account with "Connect to Watch" enabled on their
|
||||||
|
iPhone
|
||||||
|
- **Need Premium:** The current account is not a premium account
|
||||||
|
- **Need 2FA item:** The current account doesn't have any cipher with TOTP set up
|
||||||
|
- **Syncing:** Displayed when changing accounts and syncing the new vault TOTPs
|
||||||
|
- **Need Device Owner Auth:** The user needs to set up an Apple Watch Passcode in order to use the
|
||||||
|
app
|
||||||
|
|
||||||
|
## Persistence and encryption
|
||||||
|
|
||||||
|
On the Watch [CoreData](https://developer.apple.com/documentation/coredata) is used as persistence
|
||||||
|
for the ciphers. So in order to encrypt the data in them a Value Transformer in each encrypted
|
||||||
|
attribute is used: `StringEncryptionTransformer`.
|
||||||
|
|
||||||
|
Inside the transformer a call to the `CryptoService` is used that ends up using
|
||||||
|
[AES.GCM](https://developer.apple.com/documentation/cryptokit/aes/gcm) to encrypt the data with a
|
||||||
|
256 bits [SymmetricKey](https://developer.apple.com/documentation/cryptokit/symmetrickey). The key
|
||||||
|
is generated/loaded the first time something needs to be encrypted and stored in the device
|
||||||
|
Keychain.
|
||||||
|
|
||||||
|
## Crash reporting
|
||||||
|
|
||||||
|
On all the other mobile applications, [AppCenter](https://appcenter.ms/) is being used as Crash
|
||||||
|
reporting tool. However, it doesn't have support for watchOS (nor its internal library to handle
|
||||||
|
crashes).
|
||||||
|
|
||||||
|
So, on the watchOS app [Firebase Crashlytics](https://firebase.google.com/docs/crashlytics) is used
|
||||||
|
with basic crash reporting enabled (there is no handled error logging here yet). For this to work a
|
||||||
|
`GoogleService-Info.plist` file is needed which is injected on the CI.
|
||||||
|
|
||||||
|
At the moment of writing this document, no plist is configured for dev environment so `Crashlytics`
|
||||||
|
is enabled on **non-DEBUG** configurations.
|
||||||
|
|
||||||
|
There is a `Log` class to log errors happened in the app, but it's only enabled in **DEBUG**
|
||||||
|
configuration.
|
||||||
BIN
docs/getting-started/android/android-sdk.png
Normal file
BIN
docs/getting-started/android/android-sdk.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 350 KiB |
177
docs/getting-started/android/index.md
Normal file
177
docs/getting-started/android/index.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# Android
|
||||||
|
|
||||||
|
:::warning Legacy
|
||||||
|
|
||||||
|
Getting started the **legacy** Android app done in .NET MAUI.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
Before you start, you should have the recommended [Tools and Libraries](../../../tools/index.md)
|
||||||
|
installed. You will also need to install:
|
||||||
|
|
||||||
|
1. Visual Studio 2022 / VS Code
|
||||||
|
2. [.NET 8 (latest)](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
|
||||||
|
- Note: Even if you have an ARM64 based Mac (M1, M2, M3, etc.), you can install all x64 SDKs to
|
||||||
|
run Android
|
||||||
|
- On Visual Studio for Mac you may need to turn on the feature for .NET 8 by going to Visual
|
||||||
|
Studio > Preferences > Preview Features > Use the .NET 8 SDK
|
||||||
|
3. .NET MAUI Workload
|
||||||
|
- You can install this by running `dotnet workload install maui`
|
||||||
|
4. Android SDK 34
|
||||||
|
- You can use the SDK manager in [Visual Studio][xamarin-vs], or [Android
|
||||||
|
Studio][android-studio] to install this
|
||||||
|
|
||||||
|
To make sure you have the Android SDK and Emulator installed:
|
||||||
|
|
||||||
|
1. Open Visual Studio
|
||||||
|
2. Click Tools > SDK Manager (under the Android subheading)
|
||||||
|
3. Click the Tools tab
|
||||||
|
4. Make sure the following items are installed:
|
||||||
|
|
||||||
|
- Android SDK tools (at least one version of the command-line tools)
|
||||||
|
- Android SDK Platform-Tools
|
||||||
|
- Android SDK Build Tools (at least one version)
|
||||||
|
- Android Emulator
|
||||||
|
|
||||||
|
5. Click Apply Changes if you've marked anything for installation
|
||||||
|
|
||||||
|
If you've missed anything, Visual Studio should prompt you anyway.
|
||||||
|
|
||||||
|
## Android Development Setup
|
||||||
|
|
||||||
|
To set up a new virtual Android device for debugging:
|
||||||
|
|
||||||
|
1. Click Tools > Device Manager (under the Android subheading)
|
||||||
|
2. Click New Device
|
||||||
|
3. Set up the device you want to emulate - you can just choose the Base Device and leave the
|
||||||
|
default settings if you're unsure
|
||||||
|
4. Visual Studio will then download the image for that device. The download progress is shown in
|
||||||
|
the progress in the Android Device Manager dialog.
|
||||||
|
5. Once this has completed, the emulated Android device will be available as a build target under
|
||||||
|
App > Debug > (name of device)
|
||||||
|
|
||||||
|
### ARM64 Macs
|
||||||
|
|
||||||
|
1. Install and open Android Studio
|
||||||
|
2. In the top navbar, click on Android Studio > Settings > Appearance & Behavior (tab) > System
|
||||||
|
Settings > Android SDK
|
||||||
|
3. In the SDK Platforms tab, ensure the "Show Package Details" checkbox is checked (located in the
|
||||||
|
bottom-right)
|
||||||
|
4. Bellow each Android API you'll see several System Images, pick one of the `ARM 64 v8a` and wait
|
||||||
|
for it to download
|
||||||
|
5. Go to View > Tool Windows > Device Manager
|
||||||
|
6. Inside Device Manager, create a device using the previously downloaded system image
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## F-Droid
|
||||||
|
|
||||||
|
On `App.csproj` and `Core.csproj` we can now pass `/p:CustomConstants=FDROID` when
|
||||||
|
building/releasing so that the `FDROID` constant is added to the defined ones at the project level
|
||||||
|
and we can use that with precompiler directives, e.g.:
|
||||||
|
|
||||||
|
```c#
|
||||||
|
#if FDROID
|
||||||
|
// perform operation only for FDROID.
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
There are currently a few problems on Visual Studio for Mac for building correctly the projects, so
|
||||||
|
if you encounter some errors build using the CLI (previously removing bin/obj folders):
|
||||||
|
|
||||||
|
```
|
||||||
|
dotnet build -f net8.0-android -c Debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing and Debugging
|
||||||
|
|
||||||
|
### Using the Android Emulator
|
||||||
|
|
||||||
|
In order to access `localhost:<port>` resources in the Android Emulator when debugging using Visual
|
||||||
|
Studio on your Mac natively, you'll need to configure the endpoint addresses using
|
||||||
|
`<http://10.0.2.2:<port>`\> in order to access `localhost`, which maps the Android proxy by design.
|
||||||
|
|
||||||
|
[xamarin-vs]: https://learn.microsoft.com/en-us/xamarin/android/get-started/installation/android-sdk
|
||||||
|
[android-studio]: https://developer.android.com/studio/releases/platforms
|
||||||
|
|
||||||
|
### Using Server Tunneling
|
||||||
|
|
||||||
|
Instead of configuring your device or emulator, you can instead use a
|
||||||
|
[proxy tunnel to your local server](../../../server/tunnel.md) and have your app connect to it
|
||||||
|
directly.
|
||||||
|
|
||||||
|
### Push Notifications
|
||||||
|
|
||||||
|
The default configuration for the Android app is to register itself to the same environment as
|
||||||
|
Bitwarden's QA Cloud. This means that if you try to debug the app using the production endpoints you
|
||||||
|
won't be able to receive Live Sync updates or Passwordless login requests.
|
||||||
|
|
||||||
|
<Bitwarden>
|
||||||
|
|
||||||
|
So, in order to receive notifications while debugging, you have two options:
|
||||||
|
|
||||||
|
- Use QA Cloud endpoints for the Api and Identity, or
|
||||||
|
- Use a local server setup where the Api is connected to QA Azure Notification Hub
|
||||||
|
|
||||||
|
</Bitwarden>
|
||||||
|
|
||||||
|
### Testing Passwordless Locally
|
||||||
|
|
||||||
|
Before you can start testing and debugging passwordless logins, make sure your local server setup is
|
||||||
|
running correctly ([server setup](../../../server/guide.md)). You should also be able to deploy your
|
||||||
|
Android app to your device or emulator.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
Debugging and testing passwordless authentication is limited by
|
||||||
|
[push notifications](#push-notifications).
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Testing passwordless notifications:
|
||||||
|
|
||||||
|
1. Start your local server (`Api`, `Identity`, `Notifications`)
|
||||||
|
2. Make sure your mobile device can [connect to your local server](#using-server-tunneling)
|
||||||
|
3. [Start the web client](../../../clients/web-vault/index.mdx), as you will need it to make login
|
||||||
|
requests
|
||||||
|
4. Deploy the Android app to your device or emulator
|
||||||
|
5. After deployment, open the app, login to your QA account and activate passwordless login requests
|
||||||
|
in settings
|
||||||
|
6. Open the web vault using your preferred browser (ex: http://localhost:8080)
|
||||||
|
7. Enter the email address of an account that has previously authenticated on that device (i.e. is a
|
||||||
|
"known device") and click Continue. When presented with the login options, click click Login with
|
||||||
|
Device.
|
||||||
|
8. Check mobile device for the notification
|
||||||
|
|
||||||
|
<Bitwarden>
|
||||||
|
|
||||||
|
## AndroidX Credentials
|
||||||
|
|
||||||
|
Currently, the
|
||||||
|
[androidx.credentials](https://developer.android.com/jetpack/androidx/releases/credentials) official
|
||||||
|
binding has some bugs and we cannot use it yet. Because of this, we made a binding ourselves which
|
||||||
|
is located in here:
|
||||||
|
[Xamarin.AndroidX.Credentials](https://github.com/bitwarden/xamarin.androidx.credentials).
|
||||||
|
|
||||||
|
As of today, we are using version 1.2.0.
|
||||||
|
|
||||||
|
In the projects, the package is added as a local NuGet package located in
|
||||||
|
`lib/android/Xamarin.AndroidX.Credentials` and this source is already configured in the
|
||||||
|
`nuget.config` file.
|
||||||
|
|
||||||
|
In the case a change is needed on the binding, create a new local NuGet package and replace it in
|
||||||
|
the aforementioned source.
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
|
||||||
|
Do not add the project to the solution and as a project reference to the `App.csproj` /
|
||||||
|
`Core.csproj` this will strangely make the iOS app crash on start because of solution configuration.
|
||||||
|
Even though we couldn't find the root cause, this is the effect caused by this action.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
</Bitwarden>
|
||||||
107
docs/getting-started/index.md
Normal file
107
docs/getting-started/index.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
# .NET MAUI (legacy)
|
||||||
|
|
||||||
|
:::warning Legacy
|
||||||
|
|
||||||
|
Getting started the **legacy** mobile app done in .NET MAUI.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Configure Git blame
|
||||||
|
|
||||||
|
We recommend that you configure git to ignore the Prettier revision:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Android Development
|
||||||
|
|
||||||
|
See the [Android Mobile app](./android/index.md) page to set up an Android development environment.
|
||||||
|
|
||||||
|
## iOS Development
|
||||||
|
|
||||||
|
<Bitwarden>
|
||||||
|
|
||||||
|
See the [iOS Mobile app](./ios/index.mdx) page to set up an iOS development environment.
|
||||||
|
|
||||||
|
</Bitwarden>
|
||||||
|
|
||||||
|
<Community>
|
||||||
|
|
||||||
|
Unfortunately, iOS development requires provisioning profiles and other capabilities only available
|
||||||
|
to internal team members. We do not have any documentation for community developers at this time.
|
||||||
|
|
||||||
|
</Community>
|
||||||
|
|
||||||
|
## watchOS Development
|
||||||
|
|
||||||
|
<Bitwarden>
|
||||||
|
|
||||||
|
See the [watchOS app](./watchos) page to set up an watchOS development environment.
|
||||||
|
|
||||||
|
</Bitwarden>
|
||||||
|
|
||||||
|
<Community>
|
||||||
|
|
||||||
|
Unfortunately, watchOS development requires provisioning profiles and other capabilities only
|
||||||
|
available to internal team members. We do not have any documentation for community developers at
|
||||||
|
this time.
|
||||||
|
|
||||||
|
</Community>
|
||||||
|
|
||||||
|
## Unit tests
|
||||||
|
|
||||||
|
:::info TL;DR;
|
||||||
|
|
||||||
|
In order to run unit tests add the argument `/p:CustomConstants=UT` on the `dotnet` command for
|
||||||
|
building/running. To work on Unit testing or use a Test runner uncomment the `CustomConstants` line
|
||||||
|
on the `Directory.Build.props`
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Given that the `Core.csproj` is a MAUI project with `net8.0-android;net8.0-ios` target frameworks
|
||||||
|
and we need `net8.0` for the tests we need a way to add that. The `Core.Test.csproj` has `net8.0` as
|
||||||
|
a target so by adding the the argument `/p:CustomConstants=UT` we add `UT` as a constant to use in
|
||||||
|
the projects. With that in place the next things happen:
|
||||||
|
|
||||||
|
- `UT` is added as a constant to use by precompiler directives
|
||||||
|
- `Core.csproj` is changed to add `net8.0` as a target framework for unit tests
|
||||||
|
- `FFImageLoading` is removed as a reference given that it doesn't support `net8.0`. Because of
|
||||||
|
this, now we have a wrapped `CachedImage` that uses the library one if it's not `UT` and a custom
|
||||||
|
one with NOOP implementation for `UT`
|
||||||
|
|
||||||
|
So if one wants to build the test project, one needs to go to `test/Core.Test` and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build -f net8.0 /p:CustomConstants=UT
|
||||||
|
```
|
||||||
|
|
||||||
|
and to run the tests go to the same folder and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test -f net8.0 /p:CustomConstants=UT
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, when working on the `Core.Test` project or when wanting to use a Test runner, go to the
|
||||||
|
`Directory.Build.props` (located in the root) and uncomment the line referencing `CustomConstants`
|
||||||
|
so that everything is loaded accordingly in the project. Because of some issues, the referenced
|
||||||
|
projects, e.g. `Core`, are only included when the `UT` constant is in place. By uncommenting this
|
||||||
|
line the projects will be referenced and one can work on that project or run the tests from a Test
|
||||||
|
runner.
|
||||||
|
|
||||||
|
## Custom constants
|
||||||
|
|
||||||
|
There are custom constants to be used by the parameter `/p:CustomConstants={Value}` when
|
||||||
|
building/running/releasing:
|
||||||
|
|
||||||
|
- `FDROID`: This is used to indicate that it's and F-Droid build/release
|
||||||
|
([want to know more?](./android/index.md#f-droid))
|
||||||
|
- `UT`: This is used when building/running the test projects or when working on one of them
|
||||||
|
([want to know more?](#unit-tests))
|
||||||
|
|
||||||
|
These constants are added to the defined ones, so anyone can use them in the code with precompiler
|
||||||
|
directives.
|
||||||
440
docs/getting-started/ios/index.mdx
Normal file
440
docs/getting-started/ios/index.mdx
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
---
|
||||||
|
sidebar_custom_props:
|
||||||
|
access: bitwarden
|
||||||
|
---
|
||||||
|
|
||||||
|
import Tabs from "@theme/Tabs";
|
||||||
|
import TabItem from "@theme/TabItem";
|
||||||
|
|
||||||
|
# iOS
|
||||||
|
|
||||||
|
:::warning Legacy
|
||||||
|
|
||||||
|
Getting started the **legacy** iOS app done in .NET MAUI.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
1. Visual Studio 2022 / VS Code
|
||||||
|
2. [.NET 8 (latest)](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
|
||||||
|
- On Visual Studio for Mac you may need to turn on the feature for .NET 8 by going to Visual
|
||||||
|
Studio > Preferences > Preview Features > Use the .NET 8 SDK
|
||||||
|
3. .NET MAUI Workload
|
||||||
|
- You can install this by running `dotnet workload install maui`
|
||||||
|
4. A Mac with Xcode 15.0 installed
|
||||||
|
|
||||||
|
## Apple Developer Account Setup
|
||||||
|
|
||||||
|
1. Accept your invite to the Bitwarden Apple Developer team. You should get a request in your email
|
||||||
|
with the subject "You're invited to join a development team." Click the link, "Accept Invitation"
|
||||||
|
and you'll be prompted to create an Apple ID for your Bitwarden email address. If you didn't
|
||||||
|
receive this email, contact the IT department (@IT in slack). Accept the terms and conditions and
|
||||||
|
complete the sign up flow
|
||||||
|
|
||||||
|
2. Go to [Apple ID Online](https://appleid.apple.com/) and log in with your new Apple ID. Set up
|
||||||
|
2-factor authentication (using mobile phone and/or trusted device) - this is critical because
|
||||||
|
Apple no longer allows "developer" accounts without MFA, but it won't tell you that when your
|
||||||
|
build fails locally
|
||||||
|
|
||||||
|
3. Go to [App Store Connect](https://appstoreconnect.apple.com/) and accept the terms and conditions
|
||||||
|
|
||||||
|
4. Ensure you have access to the Bitwarden team and team app profiles
|
||||||
|
|
||||||
|
5. Go to [Apple Developer Account](https://developer.apple.com/account/) and go to the
|
||||||
|
"Certificates, IDs & Profiles" menu item. Check that you can see the 8bit Solutions LLC
|
||||||
|
Certificates in the Certificates section, and the Bitwarden profiles in the Profiles section. If
|
||||||
|
any of this is missing, ask the IT department (@IT #tech-support in slack) for the additional
|
||||||
|
roles / permissions
|
||||||
|
|
||||||
|
## macOS Setup
|
||||||
|
|
||||||
|
Next, you need to get your Mac environment set up for building and running the Bitwarden iOS mobile
|
||||||
|
project. This requires creating the necessary developer provisioning profiles for code signing and
|
||||||
|
execution on your Mac through Xcode. Visual Studio has a simple process to get all of the
|
||||||
|
provisioning profiles, however this is prone to fail without much feedback. Try the Visual Studio
|
||||||
|
instructions ("The Easy Way") first, and fallback to the Xcode instructions (“The Hard Way”) if
|
||||||
|
required.
|
||||||
|
|
||||||
|
### Visual Studio: The Easy Way
|
||||||
|
|
||||||
|
1. Open Visual Studio for Mac
|
||||||
|
|
||||||
|
2. Go to Preferences > Publishing > Apple Developer Accounts
|
||||||
|
|
||||||
|
3. Click “Add”, choose "Enterprise Account", and sign in with your previously configured Apple
|
||||||
|
Developer account
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
If you receive a "Failed to synchronize with Apple Developer Portal" error, you’re missing
|
||||||
|
additional roles / permissions.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
After signing in successfully, you should see your account in the list and “Bitwarden Inc” in
|
||||||
|
the account teams list
|
||||||
|
|
||||||
|
4. Click “View Details…”
|
||||||
|
|
||||||
|
5. If you don’t have a valid Apple Development certificate, click Create certificate > Apple
|
||||||
|
Development
|
||||||
|
|
||||||
|
6. Click “Download All Profiles”
|
||||||
|
|
||||||
|
7. You should now be able to run the app by setting
|
||||||
|
`iOS > Debug | iPhone Simulator > [pick any iOS Simulator]` in the top left corner and pressing
|
||||||
|
Play
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
If this worked, you can skip the next section.
|
||||||
|
|
||||||
|
If you only have the option "Generic Simulator", with a message to lower the 'Deployment Target',
|
||||||
|
your version of MAUI may not yet support the version of Xcode that you are using (as discussed
|
||||||
|
[here](https://github.com/xamarin/xamarin-macios/issues/15954#issuecomment-1246025735)).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
To work around this issue, try [downloading](https://developer.apple.com/download/all/) and
|
||||||
|
installing an older version of Xcode from Apple (you can look for guidance on which Xcode version to
|
||||||
|
use from the Xamarin.iOS [release notes](https://github.com/xamarin/xamarin-macios/releases) (this
|
||||||
|
applies to MAUI as well). After installing the new version of Xcode, restart Visual Studio and load
|
||||||
|
your project to verify your available simulator options.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
If you need multiple versions of Xcode installed on your development machine, you can rename the
|
||||||
|
`Xcode.app` file extracted from your download to something else (e.g. "Xcode_14_2.app") before
|
||||||
|
placing it in your Applications folder. You can then switch between Xcode versions by using
|
||||||
|
`xcode-select` from the command line. e.g.:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo xcode-select -s /Applications/Xcode_14_2.app
|
||||||
|
```
|
||||||
|
|
||||||
|
You may achieve similar results with tooling such as
|
||||||
|
[Xcodes.app](https://github.com/XcodesOrg/XcodesApp)
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Xcode: The Hard Way
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
If you're the next person to follow these instructions, please commit and upload the Xcode project
|
||||||
|
files you create so we can streamline this process.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Only try these instructions if the Visual Studio instructions above didn't work for you.
|
||||||
|
|
||||||
|
1. Open Xcode
|
||||||
|
|
||||||
|
2. Accept any defaults, ensure any extensions/add-ons have been installed, etc.
|
||||||
|
|
||||||
|
3. Create new project... > iOS > App
|
||||||
|
|
||||||
|
4. Use the following options for your new project:
|
||||||
|
|
||||||
|
- Product Name: "bitwarden"
|
||||||
|
|
||||||
|
- Team: Bitwarden Inc (if this is missing, double check your Apple Developer Account setup
|
||||||
|
above)
|
||||||
|
|
||||||
|
- Organization Identifier: "com.8bit"
|
||||||
|
|
||||||
|
- Bundle Identifier (automatically generated): "com.8bit.bitwarden"
|
||||||
|
|
||||||
|
- Language: Objective-C
|
||||||
|
|
||||||
|
- User Interface: Storyboard
|
||||||
|
|
||||||
|
- Leave all other checkboxes unchecked (or uncheck them)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
5. Click Next, save to the default location and then click "Create"
|
||||||
|
|
||||||
|
6. On the project configuration page, click the "Signing & Capabilities" tab
|
||||||
|
|
||||||
|
7. Make sure you have the following defaults:
|
||||||
|
|
||||||
|
- Automatically manage signing: (checked)
|
||||||
|
|
||||||
|
- Team: Bitwarden Inc
|
||||||
|
|
||||||
|
- Provisioning Profile: Xcode Managed Profile
|
||||||
|
|
||||||
|
- Signing Certificate: your Apple ID/Name
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
8. From the menu bar, click Product > Build
|
||||||
|
|
||||||
|
9. Repeat Steps 3-8, with the following changes in step 4:
|
||||||
|
|
||||||
|
- Product Name: "find-login-action-extension"
|
||||||
|
|
||||||
|
- Organization Identifier: "com.8bit.bitwarden"
|
||||||
|
|
||||||
|
- Bundle Identifier (automatically generated): "com.8bit.bitwarden.find-login-action-extension"
|
||||||
|
|
||||||
|
10. Repeat Steps 3-8, with the following changes in step 4:
|
||||||
|
|
||||||
|
- Product Name: "autofill"
|
||||||
|
|
||||||
|
- Organization Identifier: "com.8bit.bitwarden"
|
||||||
|
|
||||||
|
- Bundle Identifier (automatically generated): "com.8bit.bitwarden.autofill"
|
||||||
|
|
||||||
|
11. Repeat Steps 3-8, with the following changes in step 4:
|
||||||
|
|
||||||
|
- Product Name: "share-extension"
|
||||||
|
|
||||||
|
- Organization Identifier: "com.8bit.bitwarden"
|
||||||
|
|
||||||
|
- Bundle Identifier (automatically generated): "com.8bit.bitwarden.share-extension"
|
||||||
|
|
||||||
|
12. If you have a physical device (e.g. iPhone or iPad) that you want to use for testing, you will
|
||||||
|
also need to do the following for each of the Xcode projects you just created:
|
||||||
|
|
||||||
|
- connect the device with a cable
|
||||||
|
|
||||||
|
- select your device as as the build target in Xcode
|
||||||
|
|
||||||
|
- from the menu bar, click Product > Build
|
||||||
|
|
||||||
|
- agree to register your device if asked
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
Sometimes these profiles can mess up. If you have issues running on your physical device (or
|
||||||
|
simulator) try running `rm -r ~/Library/MobileDevice/Provisioning\ Profiles` to clear them out.
|
||||||
|
Build each Xcode project again to regenerate them.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Visual Studio
|
||||||
|
|
||||||
|
Next, we need to configure your Visual Studio environment for development.
|
||||||
|
|
||||||
|
<Tabs groupId="os">
|
||||||
|
<TabItem value="win" label="Windows" default>
|
||||||
|
|
||||||
|
1. Connect to the Mac that you just completed the above steps on
|
||||||
|
|
||||||
|
2. Open Visual Studio and click Tools > iOS > Pair to Mac
|
||||||
|
|
||||||
|
3. Scan for and select your machine. If you don't see it, click the "Add Mac..." button and put in
|
||||||
|
the Mac name or IP address. If you don't know your Mac name (or you're in a Windows VM on your
|
||||||
|
Mac), go to your Mac and open System Preferences > Sharing and look for the ".local" address of
|
||||||
|
your machine
|
||||||
|
|
||||||
|
4. Provide your Username and Password for macOS when prompted
|
||||||
|
|
||||||
|
5. Once paired, close the Pair Mac window
|
||||||
|
|
||||||
|
6. Change your active build profile to Debug > iPhoneSimulator > iOS
|
||||||
|
|
||||||
|
7. Rebuild the iOS project from Solution Explorer
|
||||||
|
|
||||||
|
8. You can now debug using the iOS Simulator
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="mac" label="macOS">
|
||||||
|
|
||||||
|
1. Check that command line tools are installed:
|
||||||
|
|
||||||
|
1. Open Xcode
|
||||||
|
|
||||||
|
2. From the menu bar, click Xcode > Preferences > Locations
|
||||||
|
|
||||||
|
3. Make sure an Xcode version is selected under "Command Line Tools"
|
||||||
|
|
||||||
|
2. Open Visual Studio for Mac
|
||||||
|
|
||||||
|
3. Open the mobile solution file (`bitwarden-mobile.sln`) in the root of your local mobile
|
||||||
|
repository
|
||||||
|
|
||||||
|
4. In the top bar, you should be able to select App > Debug > select your model and click run (or
|
||||||
|
your physical device if you set one up)
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To build from the CLI, navigate to the application directory:
|
||||||
|
|
||||||
|
For device:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd src/App
|
||||||
|
dotnet build -f net8.0-ios -c Debug -r ios-arm64
|
||||||
|
```
|
||||||
|
|
||||||
|
For simulator:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd src/App
|
||||||
|
dotnet build -f net8.0-ios -c Debug -r iossimulator-x64
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use the IDE but keep in mind:
|
||||||
|
|
||||||
|
:::tip Visual Studio for Mac
|
||||||
|
|
||||||
|
There are currently a few problems on Visual Studio for Mac for building correctly the projects, so
|
||||||
|
if you encounter some errors, build using the CLI being into the `src/App` folder (previously
|
||||||
|
removing bin/obj folders).
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::tip Argon2Id
|
||||||
|
|
||||||
|
If you find any errors regarding argon2Id library when building for simulator, please be sure that
|
||||||
|
you are building for runtime identifier `iossimulator-x64` as currently the library doesn't support
|
||||||
|
`iossimulator-arm64`.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::tip Troubleshooting common mistakes
|
||||||
|
|
||||||
|
If you find the next error:
|
||||||
|
|
||||||
|
> `error NETSDK1134`: Building a solution with a specific RuntimeIdentifier is not supported. If you
|
||||||
|
> would like to publish for a single RID, specify the RID at the individual project level instead
|
||||||
|
|
||||||
|
you almost surely are trying to build the app from the root folder. Instead go to `src/App` and try
|
||||||
|
building again.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Argon2Id library loading
|
||||||
|
|
||||||
|
The Argon2Id library (`libargon2.a`) is loaded using `MTouchExtraArgs` in almost all projects of the
|
||||||
|
solution. In order to make this simpler a property was added into **Directory.Build.props** called
|
||||||
|
`Argon2IdLoadMtouchExtraArgs` which has the code to fill in the extra args parameter. Each project
|
||||||
|
is configured with this property so this is only added on the correct runtime identifiers and we can
|
||||||
|
build the app successfully on each case.
|
||||||
|
|
||||||
|
### Ignoring extensions / watchOS app
|
||||||
|
|
||||||
|
Sometimes we need to quickly build the app or maybe some configuration on the iOS extensions or the
|
||||||
|
watchOS app gets in the way. In order to have a fast way to only care about the main app two
|
||||||
|
properties were added to the **Directory.Build.Props** to help with this:
|
||||||
|
|
||||||
|
- `IncludeBitwardeniOSExtensions`: If `True` then all the iOS extensions will be included on the
|
||||||
|
building of the main app, otherwise they will be skipped.
|
||||||
|
- `IncludeBitwardenWatchOSApp`: If `True` then the watchOS app will be included on the building of
|
||||||
|
the main app, otherwise it will be skipped.
|
||||||
|
|
||||||
|
:::warning Shared code
|
||||||
|
|
||||||
|
Toggling these off can provide a faster developer experience which is really useful in a lot of
|
||||||
|
scenarios, but always bear in mind that a lot of things are shared between the main app and the
|
||||||
|
extensions so before pushing your work, test again with everything enabled just in case.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Release mode locally
|
||||||
|
|
||||||
|
There are some issues that require us to build the app on **Release** configuration but locally
|
||||||
|
without going through the CI/CD pipeline. The problem is that we don't have the code signing details
|
||||||
|
for Distribution locally. To overcome this we can use the same `CodesignProvision` and `CodesignKey`
|
||||||
|
we use for **Debug** but on the **Release** config. The thing is that it's a bit cumbersome to
|
||||||
|
change that on every project so two properties were added to the **Directory.Build.Props** to help
|
||||||
|
with this:
|
||||||
|
|
||||||
|
- `ReleaseCodesignProvision`: `CodesignProvision` for Release config on all projects
|
||||||
|
- `ReleaseCodesignKey`: `CodesignKey` for Release config on all projects
|
||||||
|
|
||||||
|
By replacing their values, all projects will have their values applied so it's easier to build the
|
||||||
|
app in **Release** mode locally.
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
### iPhone Simulator
|
||||||
|
|
||||||
|
The iPhone Simulator has access to localhost and you can point the client at your local dev server
|
||||||
|
as usual. However, the app will require https by default. To allow http for testing purposes, follow
|
||||||
|
these steps.
|
||||||
|
|
||||||
|
1. Open `src/App/Platforms/iOS/Info.plist` in Visual Studio Code or another editor so that you can
|
||||||
|
edit the raw XML. (Don't use the Property List Editor in Visual Studio.)
|
||||||
|
|
||||||
|
2. Add the following code in the top-level `<dict>` element:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSExceptionDomains</key>
|
||||||
|
<dict>
|
||||||
|
<key>localhost</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSIncludesSubdomains</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Save and exit `Info.plist`
|
||||||
|
|
||||||
|
4. Press <kbd>Command</kbd> + <kbd>B</kbd> to force a new build before launching
|
||||||
|
|
||||||
|
5. Don't push these changes :)
|
||||||
|
|
||||||
|
### iPhone device
|
||||||
|
|
||||||
|
The device doesn’t have direct access to your Mac’s localhost, so you can follow
|
||||||
|
[this guide to connect them](https://ymoondhra.medium.com/how-to-run-localhost-on-your-iphone-4110a54d1896).
|
||||||
|
|
||||||
|
After you do that, you’ll have to also modify the `Info.plist` to allow http for testing purposes as
|
||||||
|
explained before on the simulator testing.
|
||||||
|
|
||||||
|
It’s also highly likely that you need to change the `launchSettings.json` on Server, on `Properties`
|
||||||
|
of each project. There you need to change the `applicationUrl` of `iisSettings -> iisExpress` and of
|
||||||
|
`profiles -> Identify` so that instead of `localhost` it says `name.local` where `name` is the
|
||||||
|
computer name you set on Mac’s Sharing config.
|
||||||
|
|
||||||
|
Before you actually test on the app, open a browser and try to connect to the `Api` by going to
|
||||||
|
`http://name.local:4000/alive` . If this doesn’t work then review the steps on the guide or the
|
||||||
|
server configuration. Make sure you have your `User secrets` up to date as well.
|
||||||
|
|
||||||
|
Finally, you’ll have to configure the `Api` and `Identity` urls on the phone to use
|
||||||
|
`http://name.local:4000` and `http://name.local:33656` where `name` is the computer name you set on
|
||||||
|
Mac’s Sharing config.
|
||||||
|
|
||||||
|
### iOS Extensions
|
||||||
|
|
||||||
|
1. Set the iOS Extension project as Startup project
|
||||||
|
|
||||||
|
2. Press Run
|
||||||
|
|
||||||
|
3. You will receive a popup saying "Waiting for the debugger to connect..."
|
||||||
|
|
||||||
|
4. Don’t open the Bitwarden app (otherwise the debugger will connect to it instead of the
|
||||||
|
extension). Instead trigger the extension
|
||||||
|
|
||||||
|
5. Your extension breakpoints should now be hit
|
||||||
|
|
||||||
|
For example: if you want to debug the **iOS.Autofill** extension, you would complete steps 1 - 3,
|
||||||
|
then go to your iOS device, open a browser, go to a login, tap the key icon and open Bitwarden from
|
||||||
|
the bottom popup.
|
||||||
|
|
||||||
|
### Using Server Tunneling
|
||||||
|
|
||||||
|
Instead of configuring your device or emulator to ignore SSL certificates, you can instead use a
|
||||||
|
[proxy tunnel to your local server](../../../server/tunnel.md) and have your app connect to it
|
||||||
|
directly.
|
||||||
|
|
||||||
|
### Push Notifications (Live Sync & Passwordless)
|
||||||
|
|
||||||
|
Push notifications are not currently available for debug deployments. They are only supported on
|
||||||
|
TestFlight and production builds.
|
||||||
BIN
docs/getting-started/ios/new-project-options.png
Normal file
BIN
docs/getting-started/ios/new-project-options.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
BIN
docs/getting-started/ios/run-debug.png
Normal file
BIN
docs/getting-started/ios/run-debug.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/getting-started/ios/signing-and-capabilities.png
Normal file
BIN
docs/getting-started/ios/signing-and-capabilities.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 307 KiB |
BIN
docs/getting-started/ios/troubleshoot-generic-simulator.png
Normal file
BIN
docs/getting-started/ios/troubleshoot-generic-simulator.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
101
docs/getting-started/watchos/index.mdx
Normal file
101
docs/getting-started/watchos/index.mdx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
---
|
||||||
|
sidebar_custom_props:
|
||||||
|
access: bitwarden
|
||||||
|
---
|
||||||
|
|
||||||
|
# watchOS
|
||||||
|
|
||||||
|
:::warning Legacy
|
||||||
|
|
||||||
|
Getting started the **legacy** watchOS app done in .NET MAUI.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
Follow the [iOS Setup](../ios/index.mdx).
|
||||||
|
|
||||||
|
In order for everything to work properly **devices** are needed. On simulators the synchronization
|
||||||
|
won't work (and some other parts may not work as well). Also, have Bluetooth enabled if possible to
|
||||||
|
ease the sync between devices and the debugging communication.
|
||||||
|
|
||||||
|
It's recommended to read the
|
||||||
|
[Watch Architecture](../../../../../architecture/mobile-clients/net-maui-legacy/watchOS) as well.
|
||||||
|
|
||||||
|
## macOS Setup
|
||||||
|
|
||||||
|
Having followed the macOS setup of iOS, no additional configuration is needed given that when the
|
||||||
|
project is opened in Xcode it will automatically have the provisioning profiles set up.
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
There are two parts from where to debug:
|
||||||
|
|
||||||
|
- From Visual Studio for Mac (the iOS app)
|
||||||
|
- From Xcode (the watchOS app)
|
||||||
|
|
||||||
|
For now, there is no way to debug both apps (iOS and watchOS) at the same time given that from MAUI
|
||||||
|
there is no access to debug information of the watchOS app and from Xcode an iOS stub app is
|
||||||
|
installed on the iPhone to debug it. So, at the moment of debugging one needs to choose which part
|
||||||
|
to have information about, therefore whether to debug from VS4M or from Xcode.
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
|
||||||
|
When debugging from Xcode the MAUI iOS app will be replaced with the stub one from Xcode. So any
|
||||||
|
configuration on the iOS app will be lost (like server urls)
|
||||||
|
|
||||||
|
When debugging from VS4M, uninstall the previous watchOS app (if any) from the Apple Watch in
|
||||||
|
between builds to have it always up to date (there are times that if one doesn't uninstall the
|
||||||
|
previous watchOS app it doesn't get updated)
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
|
||||||
|
If one needs to get the logs or use the _Console_ app to see the logs from the watch then one needs
|
||||||
|
to install the `sysdiagnose` profile for watchOS from Apple Developer site
|
||||||
|
[here](https://developer.apple.com/bug-reporting/profiles-and-logs/?name=sysdiagnose) into the
|
||||||
|
paired iPhone and after that restart both devices in order for the logs to work.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
Given that the MAUI iOS app needs the output of the build of Xcode, one needs to build the watchOS
|
||||||
|
app from Xcode first and then from VS4M build the iOS app to run it on the device.
|
||||||
|
|
||||||
|
The output of Xcode build is stored in a location pretty similar to the next one that is configured
|
||||||
|
in the `iOS.csproj`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<PropertyGroup>
|
||||||
|
<WatchAppBuildPath Condition=" '$(Configuration)' == 'Debug' ">$(Home)/Library/Developer/Xcode/DerivedData/bitwarden-cbtqsueryycvflfzbsoteofskiyr/Build/Products</WatchAppBuildPath>
|
||||||
|
```
|
||||||
|
|
||||||
|
It's highly likely that the folder `bitwarden-cbtqsueryycvflfzbsoteofskiyr` won't be the same on
|
||||||
|
every Mac. So one needs to change that part in `iOS.csproj` to the one created automatically by
|
||||||
|
Xcode locally.
|
||||||
|
|
||||||
|
To know exactly which is the path: Open the Project in Xcode -> Go to Product -> Show Build Folder
|
||||||
|
in Finder.
|
||||||
|
|
||||||
|
_This needs to be improved to have a fixed location or an easier way to get it automatically._
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
|
||||||
|
One needs to take special attention to target the same platform on both IDEs. Therefore when running
|
||||||
|
on a device, target the device both in Xcode and on VS4M when building so that the watchOS app is
|
||||||
|
bundled correctly. Also one needs to make sure that "bitwarden WatchKit app" scheme is selected.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Synchronization
|
||||||
|
|
||||||
|
There is no way to debug the synchronization completely at the same time for the reasons
|
||||||
|
aforementioned.
|
||||||
|
|
||||||
|
So one can debug one end (iOS) or the other (watchOS).
|
||||||
|
|
||||||
|
If needed to check something on both ends "at the same time" (like to check why a message is not
|
||||||
|
sent/arrived), one needs to use console logging or adapt part of the MAUI code to the iOS stub app
|
||||||
|
on Xcode and debug the synchronization from Xcode.
|
||||||
6
global.json
Normal file
6
global.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"sdk": {
|
||||||
|
"version": "8.0.402",
|
||||||
|
"rollForward": "disable"
|
||||||
|
}
|
||||||
|
}
|
||||||
Submodule lib/MessagePack deleted from 1ecb15e311
19
lib/MessagePack/LICENSE.md
Normal file
19
lib/MessagePack/LICENSE.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright 2018 Read Evaluate Press, LLC
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
32
lib/MessagePack/MessagePack-FlightSchool.podspec
Normal file
32
lib/MessagePack/MessagePack-FlightSchool.podspec
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = 'MessagePack-FlightSchool'
|
||||||
|
s.module_name = 'MessagePack'
|
||||||
|
s.version = '1.2.4'
|
||||||
|
s.summary = 'A MessagePack encoder and decoder for Codable types.'
|
||||||
|
|
||||||
|
s.description = <<-DESC
|
||||||
|
This functionality is discussed in Chapter 7 of
|
||||||
|
Flight School Guide to Swift Codable.
|
||||||
|
DESC
|
||||||
|
|
||||||
|
s.homepage = 'https://flight.school/books/codable/'
|
||||||
|
|
||||||
|
s.license = { type: 'MIT', file: 'LICENSE.md' }
|
||||||
|
|
||||||
|
s.author = { 'Mattt' => 'mattt@flight.school' }
|
||||||
|
|
||||||
|
s.social_media_url = 'https://twitter.com/mattt'
|
||||||
|
|
||||||
|
s.ios.deployment_target = '8.0'
|
||||||
|
s.osx.deployment_target = '10.10'
|
||||||
|
s.watchos.deployment_target = '2.0'
|
||||||
|
s.tvos.deployment_target = '9.0'
|
||||||
|
|
||||||
|
s.source = { git: 'https://github.com/Flight-School/MessagePack.git',
|
||||||
|
tag: s.version.to_s }
|
||||||
|
|
||||||
|
s.source_files = 'Sources/**/*.swift'
|
||||||
|
|
||||||
|
s.swift_version = '4.2'
|
||||||
|
s.static_framework = true
|
||||||
|
end
|
||||||
13
lib/MessagePack/MessagePack.playground/Contents.swift
Normal file
13
lib/MessagePack/MessagePack.playground/Contents.swift
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import MessagePack
|
||||||
|
|
||||||
|
let encoder = MessagePackEncoder()
|
||||||
|
|
||||||
|
let value: String = "hello"
|
||||||
|
let encodedData = try encoder.encode(value)
|
||||||
|
|
||||||
|
print("Bytes: ", encodedData.map{ String($0, radix: 16, uppercase: true) })
|
||||||
|
|
||||||
|
let decoder = MessagePackDecoder()
|
||||||
|
let decodedValue = try decoder.decode(String.self, from: encodedData)
|
||||||
|
|
||||||
|
decodedValue == value
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<playground version='5.0' target-platform='macos' executeOnSourceChanges='false'>
|
||||||
|
<timeline fileName='timeline.xctimeline'/>
|
||||||
|
</playground>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>BNDL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>NSPrincipalClass</key>
|
||||||
|
<string></string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
25
lib/MessagePack/MessagePack.xcodeproj/MessagePack_Info.plist
Normal file
25
lib/MessagePack/MessagePack.xcodeproj/MessagePack_Info.plist
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>NSPrincipalClass</key>
|
||||||
|
<string></string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
543
lib/MessagePack/MessagePack.xcodeproj/project.pbxproj
Normal file
543
lib/MessagePack/MessagePack.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,543 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 46;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXAggregateTarget section */
|
||||||
|
"MessagePack::MessagePackPackageTests::ProductTarget" /* MessagePackPackageTests */ = {
|
||||||
|
isa = PBXAggregateTarget;
|
||||||
|
buildConfigurationList = OBJ_57 /* Build configuration list for PBXAggregateTarget "MessagePackPackageTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
OBJ_60 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = MessagePackPackageTests;
|
||||||
|
productName = MessagePackPackageTests;
|
||||||
|
};
|
||||||
|
/* End PBXAggregateTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
1BC312FF2992DE9C00177F2A /* DataSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC312FE2992DE9C00177F2A /* DataSpec.swift */; };
|
||||||
|
OBJ_38 /* AnyCodingKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* AnyCodingKey.swift */; };
|
||||||
|
OBJ_39 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* Box.swift */; };
|
||||||
|
OBJ_40 /* KeyedDecodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* KeyedDecodingContainer.swift */; };
|
||||||
|
OBJ_41 /* MessagePackDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* MessagePackDecoder.swift */; };
|
||||||
|
OBJ_42 /* SingleValueDecodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_14 /* SingleValueDecodingContainer.swift */; };
|
||||||
|
OBJ_43 /* UnkeyedDecodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* UnkeyedDecodingContainer.swift */; };
|
||||||
|
OBJ_44 /* KeyedEncodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* KeyedEncodingContainer.swift */; };
|
||||||
|
OBJ_45 /* MessagePackEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_18 /* MessagePackEncoder.swift */; };
|
||||||
|
OBJ_46 /* SingleValueEncodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_19 /* SingleValueEncodingContainer.swift */; };
|
||||||
|
OBJ_47 /* UnkeyedEncodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_20 /* UnkeyedEncodingContainer.swift */; };
|
||||||
|
OBJ_48 /* FixedWidthInteger+Bytes.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_21 /* FixedWidthInteger+Bytes.swift */; };
|
||||||
|
OBJ_55 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; };
|
||||||
|
OBJ_66 /* Airport.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_24 /* Airport.swift */; };
|
||||||
|
OBJ_67 /* MessagePackDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_25 /* MessagePackDecodingTests.swift */; };
|
||||||
|
OBJ_68 /* MessagePackEncodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_26 /* MessagePackEncodingTests.swift */; };
|
||||||
|
OBJ_69 /* MessagePackPerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_27 /* MessagePackPerformanceTests.swift */; };
|
||||||
|
OBJ_70 /* MessagePackRoundTripTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_28 /* MessagePackRoundTripTests.swift */; };
|
||||||
|
OBJ_72 /* MessagePack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "MessagePack::MessagePack::Product" /* MessagePack.framework */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
1BC312FC2989A1AD00177F2A /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = OBJ_1 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = "MessagePack::MessagePack";
|
||||||
|
remoteInfo = MessagePack;
|
||||||
|
};
|
||||||
|
1BC312FD2989A1B200177F2A /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = OBJ_1 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = "MessagePack::MessagePackTests";
|
||||||
|
remoteInfo = MessagePackTests;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
1BC312FE2992DE9C00177F2A /* DataSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSpec.swift; sourceTree = "<group>"; };
|
||||||
|
"MessagePack::MessagePack::Product" /* MessagePack.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = MessagePack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
"MessagePack::MessagePackTests::Product" /* MessagePackTests.xctest */ = {isa = PBXFileReference; lastKnownFileType = file; path = MessagePackTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
OBJ_10 /* Box.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = "<group>"; };
|
||||||
|
OBJ_12 /* KeyedDecodingContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedDecodingContainer.swift; sourceTree = "<group>"; };
|
||||||
|
OBJ_13 /* MessagePackDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePackDecoder.swift; sourceTree = "<group>"; };
|
||||||
|
OBJ_14 /* SingleValueDecodingContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleValueDecodingContainer.swift; sourceTree = "<group>"; };
|
||||||
|
OBJ_15 /* UnkeyedDecodingContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnkeyedDecodingContainer.swift; sourceTree = "<group>"; };
|
||||||
|
OBJ_17 /* KeyedEncodingContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedEncodingContainer.swift; sourceTree = "<group>"; };
|
||||||
|
OBJ_18 /* MessagePackEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePackEncoder.swift; sourceTree = "<group>"; };
|
||||||
|
OBJ_19 /* SingleValueEncodingContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleValueEncodingContainer.swift; sourceTree = "<group>"; };
|
||||||
|
OBJ_20 /* UnkeyedEncodingContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnkeyedEncodingContainer.swift; sourceTree = "<group>"; };
|
||||||
|
OBJ_21 /* FixedWidthInteger+Bytes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+Bytes.swift"; sourceTree = "<group>"; };
|
||||||
|
OBJ_24 /* Airport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Airport.swift; sourceTree = "<group>"; };
|
||||||
|
OBJ_25 /* MessagePackDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePackDecodingTests.swift; sourceTree = "<group>"; };
|
||||||
|
OBJ_26 /* MessagePackEncodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePackEncodingTests.swift; sourceTree = "<group>"; };
|
||||||
|
OBJ_27 /* MessagePackPerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePackPerformanceTests.swift; sourceTree = "<group>"; };
|
||||||
|
OBJ_28 /* MessagePackRoundTripTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePackRoundTripTests.swift; sourceTree = "<group>"; };
|
||||||
|
OBJ_29 /* MessagePack.xcworkspace */ = {isa = PBXFileReference; lastKnownFileType = wrapper.workspace; path = MessagePack.xcworkspace; sourceTree = SOURCE_ROOT; };
|
||||||
|
OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
|
||||||
|
OBJ_9 /* AnyCodingKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyCodingKey.swift; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
OBJ_49 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 0;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
OBJ_71 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 0;
|
||||||
|
files = (
|
||||||
|
OBJ_72 /* MessagePack.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
OBJ_11 /* Decoder */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
OBJ_12 /* KeyedDecodingContainer.swift */,
|
||||||
|
OBJ_13 /* MessagePackDecoder.swift */,
|
||||||
|
OBJ_14 /* SingleValueDecodingContainer.swift */,
|
||||||
|
OBJ_15 /* UnkeyedDecodingContainer.swift */,
|
||||||
|
);
|
||||||
|
path = Decoder;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
OBJ_16 /* Encoder */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
OBJ_17 /* KeyedEncodingContainer.swift */,
|
||||||
|
OBJ_18 /* MessagePackEncoder.swift */,
|
||||||
|
OBJ_19 /* SingleValueEncodingContainer.swift */,
|
||||||
|
OBJ_20 /* UnkeyedEncodingContainer.swift */,
|
||||||
|
);
|
||||||
|
path = Encoder;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
OBJ_22 /* Tests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
OBJ_23 /* MessagePackTests */,
|
||||||
|
);
|
||||||
|
name = Tests;
|
||||||
|
sourceTree = SOURCE_ROOT;
|
||||||
|
};
|
||||||
|
OBJ_23 /* MessagePackTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
OBJ_24 /* Airport.swift */,
|
||||||
|
OBJ_25 /* MessagePackDecodingTests.swift */,
|
||||||
|
OBJ_26 /* MessagePackEncodingTests.swift */,
|
||||||
|
OBJ_27 /* MessagePackPerformanceTests.swift */,
|
||||||
|
OBJ_28 /* MessagePackRoundTripTests.swift */,
|
||||||
|
);
|
||||||
|
name = MessagePackTests;
|
||||||
|
path = Tests/MessagePackTests;
|
||||||
|
sourceTree = SOURCE_ROOT;
|
||||||
|
};
|
||||||
|
OBJ_30 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
"MessagePack::MessagePackTests::Product" /* MessagePackTests.xctest */,
|
||||||
|
"MessagePack::MessagePack::Product" /* MessagePack.framework */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = BUILT_PRODUCTS_DIR;
|
||||||
|
};
|
||||||
|
OBJ_5 /* */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
OBJ_6 /* Package.swift */,
|
||||||
|
OBJ_7 /* Sources */,
|
||||||
|
OBJ_22 /* Tests */,
|
||||||
|
OBJ_29 /* MessagePack.xcworkspace */,
|
||||||
|
OBJ_30 /* Products */,
|
||||||
|
);
|
||||||
|
name = "";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
OBJ_7 /* Sources */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
OBJ_8 /* MessagePack */,
|
||||||
|
);
|
||||||
|
name = Sources;
|
||||||
|
sourceTree = SOURCE_ROOT;
|
||||||
|
};
|
||||||
|
OBJ_8 /* MessagePack */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
OBJ_9 /* AnyCodingKey.swift */,
|
||||||
|
OBJ_10 /* Box.swift */,
|
||||||
|
OBJ_11 /* Decoder */,
|
||||||
|
OBJ_16 /* Encoder */,
|
||||||
|
OBJ_21 /* FixedWidthInteger+Bytes.swift */,
|
||||||
|
1BC312FE2992DE9C00177F2A /* DataSpec.swift */,
|
||||||
|
);
|
||||||
|
name = MessagePack;
|
||||||
|
path = Sources/MessagePack;
|
||||||
|
sourceTree = SOURCE_ROOT;
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
"MessagePack::MessagePack" /* MessagePack */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = OBJ_34 /* Build configuration list for PBXNativeTarget "MessagePack" */;
|
||||||
|
buildPhases = (
|
||||||
|
OBJ_37 /* Sources */,
|
||||||
|
OBJ_49 /* Frameworks */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = MessagePack;
|
||||||
|
productName = MessagePack;
|
||||||
|
productReference = "MessagePack::MessagePack::Product" /* MessagePack.framework */;
|
||||||
|
productType = "com.apple.product-type.framework";
|
||||||
|
};
|
||||||
|
"MessagePack::MessagePackTests" /* MessagePackTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = OBJ_62 /* Build configuration list for PBXNativeTarget "MessagePackTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
OBJ_65 /* Sources */,
|
||||||
|
OBJ_71 /* Frameworks */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
OBJ_73 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = MessagePackTests;
|
||||||
|
productName = MessagePackTests;
|
||||||
|
productReference = "MessagePack::MessagePackTests::Product" /* MessagePackTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
"MessagePack::SwiftPMPackageDescription" /* MessagePackPackageDescription */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = OBJ_51 /* Build configuration list for PBXNativeTarget "MessagePackPackageDescription" */;
|
||||||
|
buildPhases = (
|
||||||
|
OBJ_54 /* Sources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = MessagePackPackageDescription;
|
||||||
|
productName = MessagePackPackageDescription;
|
||||||
|
productType = "com.apple.product-type.framework";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
OBJ_1 /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
LastUpgradeCheck = 9999;
|
||||||
|
};
|
||||||
|
buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "MessagePack" */;
|
||||||
|
compatibilityVersion = "Xcode 3.2";
|
||||||
|
developmentRegion = English;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
English,
|
||||||
|
en,
|
||||||
|
);
|
||||||
|
mainGroup = OBJ_5 /* */;
|
||||||
|
productRefGroup = OBJ_30 /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
"MessagePack::MessagePack" /* MessagePack */,
|
||||||
|
"MessagePack::SwiftPMPackageDescription" /* MessagePackPackageDescription */,
|
||||||
|
"MessagePack::MessagePackPackageTests::ProductTarget" /* MessagePackPackageTests */,
|
||||||
|
"MessagePack::MessagePackTests" /* MessagePackTests */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
OBJ_37 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 0;
|
||||||
|
files = (
|
||||||
|
OBJ_38 /* AnyCodingKey.swift in Sources */,
|
||||||
|
OBJ_39 /* Box.swift in Sources */,
|
||||||
|
OBJ_40 /* KeyedDecodingContainer.swift in Sources */,
|
||||||
|
OBJ_41 /* MessagePackDecoder.swift in Sources */,
|
||||||
|
OBJ_42 /* SingleValueDecodingContainer.swift in Sources */,
|
||||||
|
OBJ_43 /* UnkeyedDecodingContainer.swift in Sources */,
|
||||||
|
OBJ_44 /* KeyedEncodingContainer.swift in Sources */,
|
||||||
|
OBJ_45 /* MessagePackEncoder.swift in Sources */,
|
||||||
|
OBJ_46 /* SingleValueEncodingContainer.swift in Sources */,
|
||||||
|
OBJ_47 /* UnkeyedEncodingContainer.swift in Sources */,
|
||||||
|
1BC312FF2992DE9C00177F2A /* DataSpec.swift in Sources */,
|
||||||
|
OBJ_48 /* FixedWidthInteger+Bytes.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
OBJ_54 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 0;
|
||||||
|
files = (
|
||||||
|
OBJ_55 /* Package.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
OBJ_65 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 0;
|
||||||
|
files = (
|
||||||
|
OBJ_66 /* Airport.swift in Sources */,
|
||||||
|
OBJ_67 /* MessagePackDecodingTests.swift in Sources */,
|
||||||
|
OBJ_68 /* MessagePackEncodingTests.swift in Sources */,
|
||||||
|
OBJ_69 /* MessagePackPerformanceTests.swift in Sources */,
|
||||||
|
OBJ_70 /* MessagePackRoundTripTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
OBJ_60 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = "MessagePack::MessagePackTests" /* MessagePackTests */;
|
||||||
|
targetProxy = 1BC312FD2989A1B200177F2A /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
OBJ_73 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = "MessagePack::MessagePack" /* MessagePack */;
|
||||||
|
targetProxy = 1BC312FC2989A1AD00177F2A /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
OBJ_3 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
|
ENABLE_NS_ASSERTIONS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
OTHER_SWIFT_FLAGS = "-DXcode";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "SWIFT_PACKAGE DEBUG";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
USE_HEADERMAP = NO;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
OBJ_35 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
|
||||||
|
);
|
||||||
|
HEADER_SEARCH_PATHS = "$(inherited)";
|
||||||
|
INFOPLIST_FILE = MessagePack.xcodeproj/MessagePack_Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx";
|
||||||
|
OTHER_CFLAGS = "$(inherited)";
|
||||||
|
OTHER_LDFLAGS = "$(inherited)";
|
||||||
|
OTHER_SWIFT_FLAGS = "$(inherited)";
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = MessagePack;
|
||||||
|
PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
|
||||||
|
SWIFT_VERSION = 4.0;
|
||||||
|
TARGET_NAME = MessagePack;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
OBJ_36 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
|
||||||
|
);
|
||||||
|
HEADER_SEARCH_PATHS = "$(inherited)";
|
||||||
|
INFOPLIST_FILE = MessagePack.xcodeproj/MessagePack_Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx";
|
||||||
|
OTHER_CFLAGS = "$(inherited)";
|
||||||
|
OTHER_LDFLAGS = "$(inherited)";
|
||||||
|
OTHER_SWIFT_FLAGS = "$(inherited)";
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = MessagePack;
|
||||||
|
PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
|
||||||
|
SWIFT_VERSION = 4.0;
|
||||||
|
TARGET_NAME = MessagePack;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
OBJ_4 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
COPY_PHASE_STRIP = YES;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
|
GCC_OPTIMIZATION_LEVEL = s;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||||
|
OTHER_SWIFT_FLAGS = "-DXcode";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||||
|
USE_HEADERMAP = NO;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
OBJ_52 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
LD = /usr/bin/true;
|
||||||
|
OTHER_SWIFT_FLAGS = "-swift-version 4 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk";
|
||||||
|
SWIFT_VERSION = 4.0;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
OBJ_53 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
LD = /usr/bin/true;
|
||||||
|
OTHER_SWIFT_FLAGS = "-swift-version 4 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk";
|
||||||
|
SWIFT_VERSION = 4.0;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
OBJ_58 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
OBJ_59 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
OBJ_63 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
EMBEDDED_CONTENT_CONTAINS_SWIFT = YES;
|
||||||
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
|
||||||
|
);
|
||||||
|
HEADER_SEARCH_PATHS = "$(inherited)";
|
||||||
|
INFOPLIST_FILE = MessagePack.xcodeproj/MessagePackTests_Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks";
|
||||||
|
OTHER_CFLAGS = "$(inherited)";
|
||||||
|
OTHER_LDFLAGS = "$(inherited)";
|
||||||
|
OTHER_SWIFT_FLAGS = "$(inherited)";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
|
||||||
|
SWIFT_VERSION = 4.0;
|
||||||
|
TARGET_NAME = MessagePackTests;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
OBJ_64 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
EMBEDDED_CONTENT_CONTAINS_SWIFT = YES;
|
||||||
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
|
||||||
|
);
|
||||||
|
HEADER_SEARCH_PATHS = "$(inherited)";
|
||||||
|
INFOPLIST_FILE = MessagePack.xcodeproj/MessagePackTests_Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks";
|
||||||
|
OTHER_CFLAGS = "$(inherited)";
|
||||||
|
OTHER_LDFLAGS = "$(inherited)";
|
||||||
|
OTHER_SWIFT_FLAGS = "$(inherited)";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
|
||||||
|
SWIFT_VERSION = 4.0;
|
||||||
|
TARGET_NAME = MessagePackTests;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
OBJ_2 /* Build configuration list for PBXProject "MessagePack" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
OBJ_3 /* Debug */,
|
||||||
|
OBJ_4 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
OBJ_34 /* Build configuration list for PBXNativeTarget "MessagePack" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
OBJ_35 /* Debug */,
|
||||||
|
OBJ_36 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
OBJ_51 /* Build configuration list for PBXNativeTarget "MessagePackPackageDescription" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
OBJ_52 /* Debug */,
|
||||||
|
OBJ_53 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
OBJ_57 /* Build configuration list for PBXAggregateTarget "MessagePackPackageTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
OBJ_58 /* Debug */,
|
||||||
|
OBJ_59 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
OBJ_62 /* Build configuration list for PBXNativeTarget "MessagePackTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
OBJ_63 /* Debug */,
|
||||||
|
OBJ_64 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = OBJ_1 /* Project object */;
|
||||||
|
}
|
||||||
7
lib/MessagePack/MessagePack.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
lib/MessagePack/MessagePack.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?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>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?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>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1010"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "MessagePack::MessagePack"
|
||||||
|
BuildableName = "MessagePack.framework"
|
||||||
|
BlueprintName = "MessagePack"
|
||||||
|
ReferencedContainer = "container:MessagePack.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "MessagePack::MessagePackTests"
|
||||||
|
BuildableName = "MessagePackTests.xctest"
|
||||||
|
BlueprintName = "MessagePackTests"
|
||||||
|
ReferencedContainer = "container:MessagePack.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "MessagePack::MessagePack"
|
||||||
|
BuildableName = "MessagePack.framework"
|
||||||
|
BlueprintName = "MessagePack"
|
||||||
|
ReferencedContainer = "container:MessagePack.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<AdditionalOptions>
|
||||||
|
</AdditionalOptions>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "MessagePack::MessagePack"
|
||||||
|
BuildableName = "MessagePack.framework"
|
||||||
|
BlueprintName = "MessagePack"
|
||||||
|
ReferencedContainer = "container:MessagePack.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<AdditionalOptions>
|
||||||
|
</AdditionalOptions>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "MessagePack::MessagePack"
|
||||||
|
BuildableName = "MessagePack.framework"
|
||||||
|
BlueprintName = "MessagePack"
|
||||||
|
ReferencedContainer = "container:MessagePack.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
10
lib/MessagePack/MessagePack.xcworkspace/contents.xcworkspacedata
generated
Normal file
10
lib/MessagePack/MessagePack.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:MessagePack.playground">
|
||||||
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:MessagePack.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?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>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?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>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
28
lib/MessagePack/Package.swift
Normal file
28
lib/MessagePack/Package.swift
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// swift-tools-version:4.0
|
||||||
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "MessagePack",
|
||||||
|
products: [
|
||||||
|
// Products define the executables and libraries produced by a package, and make them visible to other packages.
|
||||||
|
.library(
|
||||||
|
name: "MessagePack",
|
||||||
|
targets: ["MessagePack"]),
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
// Dependencies declare other packages that this package depends on.
|
||||||
|
// .package(url: /* package url */, from: "1.0.0"),
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||||
|
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
|
||||||
|
.target(
|
||||||
|
name: "MessagePack",
|
||||||
|
dependencies: []),
|
||||||
|
.testTarget(
|
||||||
|
name: "MessagePackTests",
|
||||||
|
dependencies: ["MessagePack"]),
|
||||||
|
]
|
||||||
|
)
|
||||||
87
lib/MessagePack/README.md
Normal file
87
lib/MessagePack/README.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# MessagePack
|
||||||
|
|
||||||
|
[![Build Status][build status badge]][build status]
|
||||||
|
|
||||||
|
A [MessagePack](https://msgpack.org/) encoder and decoder for `Codable` types.
|
||||||
|
|
||||||
|
This functionality is discussed in Chapter 7 of
|
||||||
|
[Flight School Guide to Swift Codable](https://flight.school/books/codable).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Swift 4.2+
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Encoding Messages
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import MessagePack
|
||||||
|
|
||||||
|
let encoder = MessagePackEncoder()
|
||||||
|
let value = try! encoder.encode(["a": 1, "b": 2, "c": 3])
|
||||||
|
// [0x83, 0xA1, 0x62, 0x02, 0xA1, 0x61, 0x01, 0xA1, 0x63, 0x03]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Decoding Messages
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import MessagePack
|
||||||
|
|
||||||
|
let decoder = MessagePackDecoder()
|
||||||
|
let data = Data(bytes: [0xCB, 0x40, 0x09, 0x21, 0xF9, 0xF0, 0x1B, 0x86, 0x6E])
|
||||||
|
let value = try! decoder.decode(Double.self, from: data)
|
||||||
|
// 3.14159
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Swift Package Manager
|
||||||
|
|
||||||
|
Add the MessagePack package to your target dependencies in `Package.swift`:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "YourProject",
|
||||||
|
dependencies: [
|
||||||
|
.package(
|
||||||
|
url: "https://github.com/Flight-School/MessagePack",
|
||||||
|
from: "1.2.3"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run the `swift build` command to build your project.
|
||||||
|
|
||||||
|
### CocoaPods
|
||||||
|
|
||||||
|
You can install `MessagePack` via CocoaPods,
|
||||||
|
by adding the following line to your `Podfile`:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
pod 'MessagePack-FlightSchool', '~> 1.2.4'
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the `pod install` command to download the library
|
||||||
|
and integrate it into your Xcode project.
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
> The module name for this library is "MessagePack" ---
|
||||||
|
> that is, to use it, you add `import MessagePack` to the top of your Swift code
|
||||||
|
> just as you would by any other installation method.
|
||||||
|
> The pod is called "MessagePack-FlightSchool"
|
||||||
|
> because there's an existing pod with the name "MessagePack".
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
Mattt ([@mattt](https://twitter.com/mattt))
|
||||||
|
|
||||||
|
[build status]: https://github.com/Flight-School/MessagePack/actions?query=workflow%3ACI
|
||||||
|
[build status badge]: https://github.com/Flight-School/MessagePack/workflows/CI/badge.svg
|
||||||
28
lib/MessagePack/Sources/MessagePack/AnyCodingKey.swift
Normal file
28
lib/MessagePack/Sources/MessagePack/AnyCodingKey.swift
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
struct AnyCodingKey: CodingKey, Equatable {
|
||||||
|
var stringValue: String
|
||||||
|
var intValue: Int?
|
||||||
|
|
||||||
|
init?(stringValue: String) {
|
||||||
|
self.stringValue = stringValue
|
||||||
|
self.intValue = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(intValue: Int) {
|
||||||
|
self.stringValue = "\(intValue)"
|
||||||
|
self.intValue = intValue
|
||||||
|
}
|
||||||
|
|
||||||
|
init<Key>(_ base: Key) where Key : CodingKey {
|
||||||
|
if let intValue = base.intValue {
|
||||||
|
self.init(intValue: intValue)!
|
||||||
|
} else {
|
||||||
|
self.init(stringValue: base.stringValue)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AnyCodingKey: Hashable {
|
||||||
|
var hashValue: Int {
|
||||||
|
return self.intValue?.hashValue ?? self.stringValue.hashValue
|
||||||
|
}
|
||||||
|
}
|
||||||
44
lib/MessagePack/Sources/MessagePack/Box.swift
Normal file
44
lib/MessagePack/Sources/MessagePack/Box.swift
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Box<Value> {
|
||||||
|
let value: Value
|
||||||
|
init(_ value: Value) {
|
||||||
|
self.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Box: Encodable where Value: Encodable {
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
try self.value.encode(to: encoder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Box: Decodable where Value: Decodable {
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
self.init(try Value(from: decoder))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Box where Value == Data {
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
self.init(try container.decode(Value.self))
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.singleValueContainer()
|
||||||
|
try container.encode(self.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Box where Value == Date {
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
self.init(try container.decode(Value.self))
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.singleValueContainer()
|
||||||
|
try container.encode(self.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
60
lib/MessagePack/Sources/MessagePack/DataSpec.swift
Normal file
60
lib/MessagePack/Sources/MessagePack/DataSpec.swift
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct DataSpec {
|
||||||
|
let name: String
|
||||||
|
let isObj: Bool
|
||||||
|
let isArray: Bool
|
||||||
|
let dataSpecBuilder: DataSpecBuilder?
|
||||||
|
|
||||||
|
init(_ name: String, _ isObj: Bool, _ isArray: Bool, _ dataSpecBuilder: DataSpecBuilder?) {
|
||||||
|
self.name = name
|
||||||
|
self.isObj = isObj
|
||||||
|
self.isArray = isArray
|
||||||
|
self.dataSpecBuilder = dataSpecBuilder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DataSpecBuilder : NSCopying {
|
||||||
|
var specs: [DataSpec] = []
|
||||||
|
var specsIterator: IndexingIterator<[DataSpec]>
|
||||||
|
|
||||||
|
init() {
|
||||||
|
specsIterator = IndexingIterator(_elements: [])
|
||||||
|
}
|
||||||
|
|
||||||
|
func append(_ name: String) -> DataSpecBuilder {
|
||||||
|
return append(DataSpec(name, false, false, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendObj(_ name: String, _ dataSpecBuilder: DataSpecBuilder) -> DataSpecBuilder {
|
||||||
|
return append(DataSpec(name, true, false, dataSpecBuilder))
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendArray(_ name: String) -> DataSpecBuilder {
|
||||||
|
return append(DataSpec(name, false, true, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendArray(_ name: String, _ dataSpecBuilder: DataSpecBuilder) -> DataSpecBuilder {
|
||||||
|
return append(DataSpec(name, false, true, dataSpecBuilder))
|
||||||
|
}
|
||||||
|
|
||||||
|
func append(_ spec: DataSpec) -> DataSpecBuilder {
|
||||||
|
specs.append(spec)
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func build() -> DataSpecBuilder {
|
||||||
|
specsIterator = specs.makeIterator()
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func next() -> DataSpec {
|
||||||
|
return specsIterator.next()!
|
||||||
|
}
|
||||||
|
|
||||||
|
public func copy(with zone: NSZone? = nil) -> Any {
|
||||||
|
let b = DataSpecBuilder()
|
||||||
|
b.specs = specs
|
||||||
|
return b.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension _MessagePackDecoder {
|
||||||
|
final class KeyedContainer<Key> where Key: CodingKey {
|
||||||
|
lazy var nestedContainers: [String: MessagePackDecodingContainer] = {
|
||||||
|
guard let count = self.count else {
|
||||||
|
return [:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var nestedContainers: [String: MessagePackDecodingContainer] = [:]
|
||||||
|
|
||||||
|
let unkeyedContainer = UnkeyedContainer(data: self.data.suffix(from: self.index), codingPath: self.codingPath, userInfo: self.userInfo)
|
||||||
|
if currentSpec != nil && currentSpec!.isObj {
|
||||||
|
unkeyedContainer.count = count
|
||||||
|
} else {
|
||||||
|
unkeyedContainer.count = count * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
var iterator = unkeyedContainer.nestedContainers.makeIterator()
|
||||||
|
|
||||||
|
for _ in 0..<count {
|
||||||
|
var key: String = ""
|
||||||
|
if currentSpec == nil || !currentSpec!.isObj {
|
||||||
|
guard let keyContainer = iterator.next() as? _MessagePackDecoder.SingleValueContainer else {
|
||||||
|
fatalError() // FIXME
|
||||||
|
}
|
||||||
|
|
||||||
|
key = try keyContainer.decode(String.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let container = iterator.next() else {
|
||||||
|
fatalError() // FIXME
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if currentSpec != nil && currentSpec!.isObj {
|
||||||
|
key = container.currentSpec!.name
|
||||||
|
}
|
||||||
|
|
||||||
|
container.codingPath += [AnyCodingKey(stringValue: key)!]
|
||||||
|
nestedContainers[key] = container
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
fatalError("\(error)") // FIXME
|
||||||
|
}
|
||||||
|
|
||||||
|
self.index = unkeyedContainer.index
|
||||||
|
|
||||||
|
return nestedContainers
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var count: Int? = {
|
||||||
|
do {
|
||||||
|
let format = try self.readByte()
|
||||||
|
|
||||||
|
if currentSpec != nil && currentSpec!.isObj && 0x90...0x9f ~= format {
|
||||||
|
return Int(format & 0x0F)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case 0x80...0x8f:
|
||||||
|
return Int(format & 0x0F)
|
||||||
|
case 0xde:
|
||||||
|
return Int(try read(UInt16.self))
|
||||||
|
case 0xdf:
|
||||||
|
return Int(try read(UInt32.self))
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var data: Data
|
||||||
|
var index: Data.Index
|
||||||
|
var codingPath: [CodingKey]
|
||||||
|
var userInfo: [CodingUserInfoKey: Any]
|
||||||
|
var currentSpec: DataSpec?
|
||||||
|
|
||||||
|
func nestedCodingPath(forKey key: CodingKey) -> [CodingKey] {
|
||||||
|
return self.codingPath + [key]
|
||||||
|
}
|
||||||
|
|
||||||
|
init(data: Data, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
self.data = data
|
||||||
|
self.index = self.data.startIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCanDecodeValue(forKey key: Key) throws {
|
||||||
|
guard self.contains(key) else {
|
||||||
|
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "key not found: \(key)")
|
||||||
|
throw DecodingError.keyNotFound(key, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _MessagePackDecoder.KeyedContainer: KeyedDecodingContainerProtocol {
|
||||||
|
var allKeys: [Key] {
|
||||||
|
return self.nestedContainers.keys.map{ Key(stringValue: $0)! }
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(_ key: Key) -> Bool {
|
||||||
|
return self.nestedContainers.keys.contains(key.stringValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeNil(forKey key: Key) throws -> Bool {
|
||||||
|
try checkCanDecodeValue(forKey: key)
|
||||||
|
|
||||||
|
let nestedContainer = self.nestedContainers[key.stringValue]
|
||||||
|
|
||||||
|
switch nestedContainer {
|
||||||
|
case let singleValueContainer as _MessagePackDecoder.SingleValueContainer:
|
||||||
|
return singleValueContainer.decodeNil()
|
||||||
|
case is _MessagePackDecoder.UnkeyedContainer,
|
||||||
|
is _MessagePackDecoder.KeyedContainer<AnyCodingKey>:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "cannot decode nil for key: \(key)")
|
||||||
|
throw DecodingError.typeMismatch(Any?.self, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
|
||||||
|
try checkCanDecodeValue(forKey: key)
|
||||||
|
|
||||||
|
let container = self.nestedContainers[key.stringValue]!
|
||||||
|
let decoder = MessagePackDecoder()
|
||||||
|
|
||||||
|
if userInfo.keys.contains(MessagePackDecoder.dataSpecKey) {
|
||||||
|
decoder.userInfo[MessagePackDecoder.dataSpecKey] = container.currentSpec!.dataSpecBuilder?.copy() as? DataSpecBuilder
|
||||||
|
if container.currentSpec!.isArray {
|
||||||
|
decoder.userInfo[MessagePackDecoder.isArrayDataSpecKey] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = try decoder.decode(T.self, from: container.data)
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
|
||||||
|
try checkCanDecodeValue(forKey: key)
|
||||||
|
|
||||||
|
guard let unkeyedContainer = self.nestedContainers[key.stringValue] as? _MessagePackDecoder.UnkeyedContainer else {
|
||||||
|
throw DecodingError.dataCorruptedError(forKey: key, in: self, debugDescription: "cannot decode nested container for key: \(key)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return unkeyedContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||||
|
try checkCanDecodeValue(forKey: key)
|
||||||
|
|
||||||
|
guard let keyedContainer = self.nestedContainers[key.stringValue] as? _MessagePackDecoder.KeyedContainer<NestedKey> else {
|
||||||
|
throw DecodingError.dataCorruptedError(forKey: key, in: self, debugDescription: "cannot decode nested container for key: \(key)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyedDecodingContainer(keyedContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func superDecoder() throws -> Decoder {
|
||||||
|
return _MessagePackDecoder(data: self.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func superDecoder(forKey key: Key) throws -> Decoder {
|
||||||
|
let decoder = _MessagePackDecoder(data: self.data)
|
||||||
|
decoder.codingPath = [key]
|
||||||
|
|
||||||
|
return decoder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _MessagePackDecoder.KeyedContainer: MessagePackDecodingContainer {}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/**
|
||||||
|
An object that decodes instances of a data type from MessagePack objects.
|
||||||
|
*/
|
||||||
|
final public class MessagePackDecoder {
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
A dictionary you use to customize the decoding process
|
||||||
|
by providing contextual information.
|
||||||
|
*/
|
||||||
|
public var userInfo: [CodingUserInfoKey : Any] = [:]
|
||||||
|
|
||||||
|
/**
|
||||||
|
Returns a value of the type you specify,
|
||||||
|
decoded from a MessagePack object.
|
||||||
|
|
||||||
|
- Parameters:
|
||||||
|
- type: The type of the value to decode
|
||||||
|
from the supplied MessagePack object.
|
||||||
|
- data: The MessagePack object to decode.
|
||||||
|
- Throws: `DecodingError.dataCorrupted(_:)`
|
||||||
|
if the data is not valid MessagePack.
|
||||||
|
*/
|
||||||
|
public func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
|
||||||
|
let decoder = _MessagePackDecoder(data: data)
|
||||||
|
decoder.userInfo = self.userInfo
|
||||||
|
decoder.userInfo[MessagePackDecoder.nonMatchingFloatDecodingStrategyKey] = nonMatchingFloatDecodingStrategy
|
||||||
|
|
||||||
|
switch type {
|
||||||
|
case is Data.Type:
|
||||||
|
let box = try Box<Data>(from: decoder)
|
||||||
|
return box.value as! T
|
||||||
|
case is Date.Type:
|
||||||
|
let box = try Box<Date>(from: decoder)
|
||||||
|
return box.value as! T
|
||||||
|
default:
|
||||||
|
return try T(from: decoder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
The strategy used by a decoder when it encounters format mismatches for floating point values.
|
||||||
|
*/
|
||||||
|
public var nonMatchingFloatDecodingStrategy: NonMatchingFloatDecodingStrategy = .strict
|
||||||
|
|
||||||
|
/**
|
||||||
|
The strategies for decoding floating point values when their format doesn't match.
|
||||||
|
*/
|
||||||
|
public enum NonMatchingFloatDecodingStrategy {
|
||||||
|
|
||||||
|
/// Throws a DecodingError.typeMismatch
|
||||||
|
case strict
|
||||||
|
|
||||||
|
/// Performs a cast
|
||||||
|
case cast
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static var nonMatchingFloatDecodingStrategyKey: CodingUserInfoKey {
|
||||||
|
return CodingUserInfoKey(rawValue: "nonMatchingFloatDecodingStrategyKey")!
|
||||||
|
}
|
||||||
|
|
||||||
|
static var dataSpecKey : CodingUserInfoKey {
|
||||||
|
return CodingUserInfoKey(rawValue: "dataSpecKey")!
|
||||||
|
}
|
||||||
|
|
||||||
|
static var isArrayDataSpecKey : CodingUserInfoKey {
|
||||||
|
return CodingUserInfoKey(rawValue: "isArrayDataSpecKey")!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TopLevelDecoder
|
||||||
|
|
||||||
|
#if canImport(Combine)
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
extension MessagePackDecoder: TopLevelDecoder {
|
||||||
|
public typealias Input = Data
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// MARK: -
|
||||||
|
|
||||||
|
final class _MessagePackDecoder {
|
||||||
|
var codingPath: [CodingKey] = []
|
||||||
|
|
||||||
|
var userInfo: [CodingUserInfoKey : Any] = [:]
|
||||||
|
|
||||||
|
var container: MessagePackDecodingContainer?
|
||||||
|
fileprivate var data: Data
|
||||||
|
|
||||||
|
init(data: Data) {
|
||||||
|
self.data = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _MessagePackDecoder: Decoder {
|
||||||
|
fileprivate func assertCanCreateContainer() {
|
||||||
|
precondition(self.container == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key>(keyedBy type: Key.Type) -> KeyedDecodingContainer<Key> where Key : CodingKey {
|
||||||
|
assertCanCreateContainer()
|
||||||
|
|
||||||
|
let container = KeyedContainer<Key>(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo)
|
||||||
|
|
||||||
|
if userInfo.keys.contains(MessagePackDecoder.dataSpecKey) {
|
||||||
|
container.currentSpec = DataSpec("", true, false, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.container = container
|
||||||
|
|
||||||
|
return KeyedDecodingContainer(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() -> UnkeyedDecodingContainer {
|
||||||
|
assertCanCreateContainer()
|
||||||
|
|
||||||
|
let container = UnkeyedContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo)
|
||||||
|
self.container = container
|
||||||
|
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() -> SingleValueDecodingContainer {
|
||||||
|
assertCanCreateContainer()
|
||||||
|
|
||||||
|
let container = SingleValueContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo)
|
||||||
|
self.container = container
|
||||||
|
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol MessagePackDecodingContainer: class {
|
||||||
|
var codingPath: [CodingKey] { get set }
|
||||||
|
|
||||||
|
var userInfo: [CodingUserInfoKey : Any] { get }
|
||||||
|
|
||||||
|
var data: Data { get set }
|
||||||
|
var index: Data.Index { get set }
|
||||||
|
|
||||||
|
var currentSpec: DataSpec? { get set }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MessagePackDecodingContainer {
|
||||||
|
func readByte() throws -> UInt8 {
|
||||||
|
return try read(1).first!
|
||||||
|
}
|
||||||
|
|
||||||
|
func read(_ length: Int) throws -> Data {
|
||||||
|
let nextIndex = self.index.advanced(by: length)
|
||||||
|
guard nextIndex <= self.data.endIndex else {
|
||||||
|
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unexpected end of data")
|
||||||
|
throw DecodingError.dataCorrupted(context)
|
||||||
|
}
|
||||||
|
defer { self.index = nextIndex }
|
||||||
|
|
||||||
|
return self.data.subdata(in: self.index..<nextIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
func read<T>(_ type: T.Type) throws -> T where T : FixedWidthInteger {
|
||||||
|
let stride = MemoryLayout<T>.stride
|
||||||
|
let bytes = [UInt8](try read(stride))
|
||||||
|
return T(bytes: bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
#if os(Linux)
|
||||||
|
let NSEC_PER_SEC: UInt64 = 1000000000
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extension _MessagePackDecoder {
|
||||||
|
final class SingleValueContainer {
|
||||||
|
var codingPath: [CodingKey]
|
||||||
|
var userInfo: [CodingUserInfoKey: Any]
|
||||||
|
var data: Data
|
||||||
|
var index: Data.Index
|
||||||
|
var currentSpec: DataSpec?
|
||||||
|
|
||||||
|
init(data: Data, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
self.data = data
|
||||||
|
self.index = self.data.startIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCanDecode<T>(_ type: T.Type, format: UInt8) throws {
|
||||||
|
guard self.index <= self.data.endIndex else {
|
||||||
|
throw DecodingError.dataCorruptedError(in: self, debugDescription: "Unexpected end of data")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard self.data[self.index] == format else {
|
||||||
|
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(format)")
|
||||||
|
throw DecodingError.typeMismatch(type, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonMatchingFloatDecodingStrategy: MessagePackDecoder.NonMatchingFloatDecodingStrategy {
|
||||||
|
return userInfo[MessagePackDecoder.nonMatchingFloatDecodingStrategyKey] as? MessagePackDecoder.NonMatchingFloatDecodingStrategy ?? .strict
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _MessagePackDecoder.SingleValueContainer: SingleValueDecodingContainer {
|
||||||
|
func decodeNil() -> Bool {
|
||||||
|
let format = try? readByte()
|
||||||
|
return format == 0xc0
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Bool.Type) throws -> Bool {
|
||||||
|
let format = try readByte()
|
||||||
|
switch format {
|
||||||
|
case 0xc2: return false
|
||||||
|
case 0xc3: return true
|
||||||
|
default:
|
||||||
|
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(format)")
|
||||||
|
throw DecodingError.typeMismatch(Bool.self, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: String.Type) throws -> String {
|
||||||
|
let length: Int
|
||||||
|
let format = try readByte()
|
||||||
|
switch format {
|
||||||
|
case 0xa0...0xbf:
|
||||||
|
length = Int(format - 0xa0)
|
||||||
|
case 0xd9:
|
||||||
|
length = Int(try read(UInt8.self))
|
||||||
|
case 0xda:
|
||||||
|
length = Int(try read(UInt16.self))
|
||||||
|
case 0xdb:
|
||||||
|
length = Int(try read(UInt32.self))
|
||||||
|
default:
|
||||||
|
throw DecodingError.dataCorruptedError(in: self, debugDescription: "Invalid format for String length: \(format)")
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = try read(length)
|
||||||
|
guard let string = String(data: data, encoding: .utf8) else {
|
||||||
|
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Couldn't decode string with UTF-8 encoding")
|
||||||
|
throw DecodingError.dataCorrupted(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Double.Type) throws -> Double {
|
||||||
|
let format = try readByte()
|
||||||
|
switch format {
|
||||||
|
case 0xca:
|
||||||
|
switch nonMatchingFloatDecodingStrategy {
|
||||||
|
case .strict:
|
||||||
|
break
|
||||||
|
case .cast:
|
||||||
|
let bitPattern = try read(UInt32.self)
|
||||||
|
return Double(Float(bitPattern: bitPattern))
|
||||||
|
}
|
||||||
|
case 0xcb:
|
||||||
|
let bitPattern = try read(UInt64.self)
|
||||||
|
return Double(bitPattern: bitPattern)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(format)")
|
||||||
|
throw DecodingError.typeMismatch(Double.self, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Float.Type) throws -> Float {
|
||||||
|
let format = try readByte()
|
||||||
|
switch format {
|
||||||
|
case 0xca:
|
||||||
|
let bitPattern = try read(UInt32.self)
|
||||||
|
return Float(bitPattern: bitPattern)
|
||||||
|
case 0xcb:
|
||||||
|
switch nonMatchingFloatDecodingStrategy {
|
||||||
|
case .strict:
|
||||||
|
break
|
||||||
|
case .cast:
|
||||||
|
let bitPattern = try read(UInt64.self)
|
||||||
|
return Float(Double(bitPattern: bitPattern))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(format)")
|
||||||
|
throw DecodingError.typeMismatch(Float.self, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode<T>(_ type: T.Type) throws -> T where T : BinaryInteger & Decodable {
|
||||||
|
let format = try readByte()
|
||||||
|
var t: T?
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case 0x00...0x7f:
|
||||||
|
t = T(format)
|
||||||
|
case 0xcc:
|
||||||
|
t = T(exactly: try read(UInt8.self))
|
||||||
|
case 0xcd:
|
||||||
|
t = T(exactly: try read(UInt16.self))
|
||||||
|
case 0xce:
|
||||||
|
t = T(exactly: try read(UInt32.self))
|
||||||
|
case 0xcf:
|
||||||
|
t = T(exactly: try read(UInt64.self))
|
||||||
|
case 0xd0:
|
||||||
|
t = T(exactly: try read(Int8.self))
|
||||||
|
case 0xd1:
|
||||||
|
t = T(exactly: try read(Int16.self))
|
||||||
|
case 0xd2:
|
||||||
|
t = T(exactly: try read(Int32.self))
|
||||||
|
case 0xd3:
|
||||||
|
t = T(exactly: try read(Int64.self))
|
||||||
|
case 0xe0...0xff:
|
||||||
|
t = T(exactly: Int8(bitPattern: format))
|
||||||
|
default:
|
||||||
|
t = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let value = t else {
|
||||||
|
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(format)")
|
||||||
|
throw DecodingError.typeMismatch(T.self, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Date.Type) throws -> Date {
|
||||||
|
let format = try readByte()
|
||||||
|
|
||||||
|
var seconds: TimeInterval
|
||||||
|
var nanoseconds: TimeInterval
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case 0xd6:
|
||||||
|
_ = try read(Int8.self) // -1
|
||||||
|
nanoseconds = 0
|
||||||
|
seconds = TimeInterval(try read(UInt32.self))
|
||||||
|
case 0xd7:
|
||||||
|
_ = try read(Int8.self) // -1
|
||||||
|
let bitPattern = try read(UInt64.self)
|
||||||
|
nanoseconds = TimeInterval(UInt32(bitPattern >> 34))
|
||||||
|
seconds = TimeInterval(UInt32(bitPattern & 0x03_FF_FF_FF_FF))
|
||||||
|
case 0xc7:
|
||||||
|
_ = try read(Int8.self) // 12
|
||||||
|
_ = try read(Int8.self) // -1
|
||||||
|
nanoseconds = TimeInterval(try read(UInt32.self))
|
||||||
|
seconds = TimeInterval(try read(Int64.self))
|
||||||
|
default:
|
||||||
|
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(format)")
|
||||||
|
throw DecodingError.typeMismatch(Date.self, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeInterval = TimeInterval(seconds) + nanoseconds / Double(NSEC_PER_SEC)
|
||||||
|
|
||||||
|
return Date(timeIntervalSince1970: timeInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Data.Type) throws -> Data {
|
||||||
|
let length: Int
|
||||||
|
let format = try readByte()
|
||||||
|
switch format {
|
||||||
|
case 0xc4:
|
||||||
|
length = Int(try read(UInt8.self))
|
||||||
|
case 0xc5:
|
||||||
|
length = Int(try read(UInt16.self))
|
||||||
|
case 0xc6:
|
||||||
|
length = Int(try read(UInt32.self))
|
||||||
|
default:
|
||||||
|
throw DecodingError.dataCorruptedError(in: self, debugDescription: "Invalid format for Data length: \(format)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.data.subdata(in: self.index..<self.index.advanced(by: length))
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode<T>(_ type: T.Type) throws -> T where T : Decodable {
|
||||||
|
switch type {
|
||||||
|
case is Data.Type:
|
||||||
|
return try decode(Data.self) as! T
|
||||||
|
case is Date.Type:
|
||||||
|
return try decode(Date.self) as! T
|
||||||
|
default:
|
||||||
|
let decoder = _MessagePackDecoder(data: self.data)
|
||||||
|
let value = try T(from: decoder)
|
||||||
|
if let nextIndex = decoder.container?.index {
|
||||||
|
self.index = nextIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _MessagePackDecoder.SingleValueContainer: MessagePackDecodingContainer {}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension _MessagePackDecoder {
|
||||||
|
final class UnkeyedContainer {
|
||||||
|
var codingPath: [CodingKey]
|
||||||
|
|
||||||
|
var nestedCodingPath: [CodingKey] {
|
||||||
|
return self.codingPath + [AnyCodingKey(intValue: self.count ?? 0)!]
|
||||||
|
}
|
||||||
|
|
||||||
|
var userInfo: [CodingUserInfoKey: Any]
|
||||||
|
|
||||||
|
var data: Data
|
||||||
|
var index: Data.Index
|
||||||
|
var currentSpec: DataSpec?
|
||||||
|
|
||||||
|
lazy var count: Int? = {
|
||||||
|
do {
|
||||||
|
let format = try self.readByte()
|
||||||
|
switch format {
|
||||||
|
case 0x90...0x9f:
|
||||||
|
return Int(format & 0x0F)
|
||||||
|
case 0xdc:
|
||||||
|
return Int(try read(UInt16.self))
|
||||||
|
case 0xdd:
|
||||||
|
return Int(try read(UInt32.self))
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var currentIndex: Int = 0
|
||||||
|
|
||||||
|
lazy var nestedContainers: [MessagePackDecodingContainer] = {
|
||||||
|
guard let count = self.count else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
var nestedContainers: [MessagePackDecodingContainer] = []
|
||||||
|
|
||||||
|
do {
|
||||||
|
for _ in 0..<count {
|
||||||
|
let container = try self.decodeContainer()
|
||||||
|
nestedContainers.append(container)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
fatalError("\(error)") // FIXME
|
||||||
|
}
|
||||||
|
|
||||||
|
self.currentIndex = 0
|
||||||
|
|
||||||
|
return nestedContainers
|
||||||
|
}()
|
||||||
|
|
||||||
|
init(data: Data, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
self.data = data
|
||||||
|
self.index = self.data.startIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
var isAtEnd: Bool {
|
||||||
|
guard let count = self.count else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentIndex >= count
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCanDecodeValue() throws {
|
||||||
|
guard !self.isAtEnd else {
|
||||||
|
throw DecodingError.dataCorruptedError(in: self, debugDescription: "Unexpected end of data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _MessagePackDecoder.UnkeyedContainer: UnkeyedDecodingContainer {
|
||||||
|
func decodeNil() throws -> Bool {
|
||||||
|
try checkCanDecodeValue()
|
||||||
|
defer { self.currentIndex += 1 }
|
||||||
|
|
||||||
|
let nestedContainer = self.nestedContainers[self.currentIndex]
|
||||||
|
|
||||||
|
switch nestedContainer {
|
||||||
|
case let singleValueContainer as _MessagePackDecoder.SingleValueContainer:
|
||||||
|
return singleValueContainer.decodeNil()
|
||||||
|
case is _MessagePackDecoder.UnkeyedContainer,
|
||||||
|
is _MessagePackDecoder.KeyedContainer<AnyCodingKey>:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "cannot decode nil for index: \(self.currentIndex)")
|
||||||
|
throw DecodingError.typeMismatch(Any?.self, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode<T>(_ type: T.Type) throws -> T where T : Decodable {
|
||||||
|
try checkCanDecodeValue()
|
||||||
|
defer { self.currentIndex += 1 }
|
||||||
|
|
||||||
|
if userInfo.keys.contains(MessagePackDecoder.isArrayDataSpecKey) {
|
||||||
|
currentSpec = DataSpec("", false, true, (userInfo[MessagePackDecoder.dataSpecKey] as? DataSpecBuilder)?.copy() as? DataSpecBuilder)
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = self.nestedContainers[self.currentIndex]
|
||||||
|
let decoder = MessagePackDecoder()
|
||||||
|
|
||||||
|
if userInfo.keys.contains(MessagePackDecoder.dataSpecKey) {
|
||||||
|
decoder.userInfo[MessagePackDecoder.dataSpecKey] = (userInfo[MessagePackDecoder.dataSpecKey] as? DataSpecBuilder)?.copy() as? DataSpecBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = try decoder.decode(T.self, from: container.data)
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
|
||||||
|
try checkCanDecodeValue()
|
||||||
|
defer { self.currentIndex += 1 }
|
||||||
|
|
||||||
|
let container = self.nestedContainers[self.currentIndex] as! _MessagePackDecoder.UnkeyedContainer
|
||||||
|
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||||
|
try checkCanDecodeValue()
|
||||||
|
defer { self.currentIndex += 1 }
|
||||||
|
|
||||||
|
let container = self.nestedContainers[self.currentIndex] as! _MessagePackDecoder.KeyedContainer<NestedKey>
|
||||||
|
|
||||||
|
return KeyedDecodingContainer(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
func superDecoder() throws -> Decoder {
|
||||||
|
return _MessagePackDecoder(data: self.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _MessagePackDecoder.UnkeyedContainer {
|
||||||
|
func decodeContainer() throws -> MessagePackDecodingContainer {
|
||||||
|
try checkCanDecodeValue()
|
||||||
|
defer { self.currentIndex += 1 }
|
||||||
|
|
||||||
|
let startIndex = self.index
|
||||||
|
|
||||||
|
var currDataSpec: DataSpec? = nil
|
||||||
|
if currentSpec != nil && currentSpec!.isArray && currentSpec!.dataSpecBuilder != nil {
|
||||||
|
currDataSpec = DataSpec("", true, false, currentSpec!.dataSpecBuilder!.copy() as? DataSpecBuilder)
|
||||||
|
} else {
|
||||||
|
let dataSpec = self.userInfo[MessagePackDecoder.dataSpecKey] as? DataSpecBuilder
|
||||||
|
if let currDS = dataSpec?.next() {
|
||||||
|
currDataSpec = DataSpec(currDS.name, currDS.isObj, currDS.isArray, currDS.dataSpecBuilder?.copy() as? DataSpecBuilder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let length: Int
|
||||||
|
let format = try self.readByte()
|
||||||
|
switch format {
|
||||||
|
case 0x00...0x7f,
|
||||||
|
0xc0, 0xc2, 0xc3,
|
||||||
|
0xe0...0xff:
|
||||||
|
length = 0
|
||||||
|
case 0xcc, 0xd0, 0xd4:
|
||||||
|
length = 1
|
||||||
|
case 0xcd, 0xd1, 0xd5:
|
||||||
|
length = 2
|
||||||
|
case 0xca, 0xce, 0xd2:
|
||||||
|
length = 4
|
||||||
|
case 0xcb, 0xcf, 0xd3:
|
||||||
|
length = 8
|
||||||
|
case 0xd6:
|
||||||
|
length = 5
|
||||||
|
case 0xd7:
|
||||||
|
length = 9
|
||||||
|
case 0xd8:
|
||||||
|
length = 16
|
||||||
|
case 0xa0...0xbf:
|
||||||
|
length = Int(format - 0xa0)
|
||||||
|
case 0xc4, 0xc7, 0xd9:
|
||||||
|
length = Int(try read(UInt8.self))
|
||||||
|
case 0xc5, 0xc8, 0xda:
|
||||||
|
length = Int(try read(UInt16.self))
|
||||||
|
case 0xc6, 0xc9, 0xdb:
|
||||||
|
length = Int(try read(UInt32.self))
|
||||||
|
case 0x80...0x8f, 0xde, 0xdf:
|
||||||
|
let container = _MessagePackDecoder.KeyedContainer<AnyCodingKey>(data: self.data.suffix(from: startIndex), codingPath: self.nestedCodingPath, userInfo: self.userInfo)
|
||||||
|
container.currentSpec = currDataSpec
|
||||||
|
_ = container.nestedContainers // FIXME
|
||||||
|
self.index = container.index
|
||||||
|
|
||||||
|
return container
|
||||||
|
case 0x90...0x9f, 0xdc, 0xdd:
|
||||||
|
if currDataSpec != nil && currDataSpec!.isObj {
|
||||||
|
var objUserInfo = self.userInfo
|
||||||
|
objUserInfo[MessagePackDecoder.dataSpecKey] = currDataSpec!.dataSpecBuilder!
|
||||||
|
|
||||||
|
let container = _MessagePackDecoder.KeyedContainer<AnyCodingKey>(data: self.data.suffix(from: startIndex), codingPath: self.nestedCodingPath, userInfo: objUserInfo)
|
||||||
|
container.currentSpec = currDataSpec
|
||||||
|
_ = container.nestedContainers // FIXME
|
||||||
|
self.index = container.index
|
||||||
|
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
var arrUserInfo = self.userInfo
|
||||||
|
if currDataSpec != nil && currDataSpec!.isArray {
|
||||||
|
arrUserInfo[MessagePackDecoder.dataSpecKey] = currDataSpec!.dataSpecBuilder!
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = _MessagePackDecoder.UnkeyedContainer(data: self.data.suffix(from: startIndex), codingPath: self.nestedCodingPath, userInfo: arrUserInfo)
|
||||||
|
container.currentSpec = currDataSpec
|
||||||
|
_ = container.nestedContainers // FIXME
|
||||||
|
|
||||||
|
self.index = container.index
|
||||||
|
|
||||||
|
return container
|
||||||
|
default:
|
||||||
|
throw DecodingError.dataCorruptedError(in: self, debugDescription: "Invalid format: \(format)")
|
||||||
|
}
|
||||||
|
|
||||||
|
let range: Range<Data.Index> = startIndex..<self.index.advanced(by: length)
|
||||||
|
self.index = range.upperBound
|
||||||
|
|
||||||
|
let container = _MessagePackDecoder.SingleValueContainer(data: self.data.subdata(in: range), codingPath: self.codingPath, userInfo: self.userInfo)
|
||||||
|
container.currentSpec = currDataSpec
|
||||||
|
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _MessagePackDecoder.UnkeyedContainer: MessagePackDecodingContainer {}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension _MessagePackEncoder {
|
||||||
|
final class KeyedContainer<Key> where Key: CodingKey {
|
||||||
|
private var storage: [AnyCodingKey: _MessagePackEncodingContainer] = [:]
|
||||||
|
|
||||||
|
var codingPath: [CodingKey]
|
||||||
|
var userInfo: [CodingUserInfoKey: Any]
|
||||||
|
|
||||||
|
func nestedCodingPath(forKey key: CodingKey) -> [CodingKey] {
|
||||||
|
return self.codingPath + [key]
|
||||||
|
}
|
||||||
|
|
||||||
|
init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _MessagePackEncoder.KeyedContainer: KeyedEncodingContainerProtocol {
|
||||||
|
func encodeNil(forKey key: Key) throws {
|
||||||
|
var container = self.nestedSingleValueContainer(forKey: key)
|
||||||
|
try container.encodeNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode<T>(_ value: T, forKey key: Key) throws where T : Encodable {
|
||||||
|
var container = self.nestedSingleValueContainer(forKey: key)
|
||||||
|
try container.encode(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func nestedSingleValueContainer(forKey key: Key) -> SingleValueEncodingContainer {
|
||||||
|
let container = _MessagePackEncoder.SingleValueContainer(codingPath: self.nestedCodingPath(forKey: key), userInfo: self.userInfo)
|
||||||
|
self.storage[AnyCodingKey(key)] = container
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
|
||||||
|
let container = _MessagePackEncoder.UnkeyedContainer(codingPath: self.nestedCodingPath(forKey: key), userInfo: self.userInfo)
|
||||||
|
self.storage[AnyCodingKey(key)] = container
|
||||||
|
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||||
|
let container = _MessagePackEncoder.KeyedContainer<NestedKey>(codingPath: self.nestedCodingPath(forKey: key), userInfo: self.userInfo)
|
||||||
|
self.storage[AnyCodingKey(key)] = container
|
||||||
|
|
||||||
|
return KeyedEncodingContainer(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
func superEncoder() -> Encoder {
|
||||||
|
fatalError("Unimplemented") // FIXME
|
||||||
|
}
|
||||||
|
|
||||||
|
func superEncoder(forKey key: Key) -> Encoder {
|
||||||
|
fatalError("Unimplemented") // FIXME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _MessagePackEncoder.KeyedContainer: _MessagePackEncodingContainer {
|
||||||
|
var data: Data {
|
||||||
|
var data = Data()
|
||||||
|
|
||||||
|
let length = storage.count
|
||||||
|
if let uint16 = UInt16(exactly: length) {
|
||||||
|
if length <= 15 {
|
||||||
|
data.append(0x80 + UInt8(length))
|
||||||
|
} else {
|
||||||
|
data.append(0xde)
|
||||||
|
data.append(contentsOf: uint16.bytes)
|
||||||
|
}
|
||||||
|
} else if let uint32 = UInt32(exactly: length) {
|
||||||
|
data.append(0xdf)
|
||||||
|
data.append(contentsOf: uint32.bytes)
|
||||||
|
} else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, container) in self.storage {
|
||||||
|
let keyContainer = _MessagePackEncoder.SingleValueContainer(codingPath: self.codingPath, userInfo: self.userInfo)
|
||||||
|
try! keyContainer.encode(key.stringValue)
|
||||||
|
data.append(keyContainer.data)
|
||||||
|
|
||||||
|
data.append(container.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/**
|
||||||
|
An object that encodes instances of a data type as MessagePack objects.
|
||||||
|
*/
|
||||||
|
final public class MessagePackEncoder {
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
A dictionary you use to customize the encoding process
|
||||||
|
by providing contextual information.
|
||||||
|
*/
|
||||||
|
public var userInfo: [CodingUserInfoKey : Any] = [:]
|
||||||
|
|
||||||
|
/**
|
||||||
|
Returns a MessagePack-encoded representation of the value you supply.
|
||||||
|
|
||||||
|
- Parameters:
|
||||||
|
- value: The value to encode as MessagePack.
|
||||||
|
- Throws: `EncodingError.invalidValue(_:_:)`
|
||||||
|
if the value can't be encoded as a MessagePack object.
|
||||||
|
*/
|
||||||
|
public func encode<T>(_ value: T) throws -> Data where T : Encodable {
|
||||||
|
let encoder = _MessagePackEncoder()
|
||||||
|
encoder.userInfo = self.userInfo
|
||||||
|
|
||||||
|
switch value {
|
||||||
|
case let data as Data:
|
||||||
|
try Box<Data>(data).encode(to: encoder)
|
||||||
|
case let date as Date:
|
||||||
|
try Box<Date>(date).encode(to: encoder)
|
||||||
|
default:
|
||||||
|
try value.encode(to: encoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
return encoder.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TopLevelEncoder
|
||||||
|
|
||||||
|
#if canImport(Combine)
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
extension MessagePackEncoder: TopLevelEncoder {
|
||||||
|
public typealias Input = Data
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// MARK: -
|
||||||
|
|
||||||
|
protocol _MessagePackEncodingContainer {
|
||||||
|
var data: Data { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MessagePackEncoder {
|
||||||
|
var codingPath: [CodingKey] = []
|
||||||
|
|
||||||
|
var userInfo: [CodingUserInfoKey : Any] = [:]
|
||||||
|
|
||||||
|
fileprivate var container: _MessagePackEncodingContainer?
|
||||||
|
|
||||||
|
var data: Data {
|
||||||
|
return container?.data ?? Data()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _MessagePackEncoder: Encoder {
|
||||||
|
fileprivate func assertCanCreateContainer() {
|
||||||
|
precondition(self.container == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey {
|
||||||
|
assertCanCreateContainer()
|
||||||
|
|
||||||
|
let container = KeyedContainer<Key>(codingPath: self.codingPath, userInfo: self.userInfo)
|
||||||
|
self.container = container
|
||||||
|
|
||||||
|
return KeyedEncodingContainer(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() -> UnkeyedEncodingContainer {
|
||||||
|
assertCanCreateContainer()
|
||||||
|
|
||||||
|
let container = UnkeyedContainer(codingPath: self.codingPath, userInfo: self.userInfo)
|
||||||
|
self.container = container
|
||||||
|
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() -> SingleValueEncodingContainer {
|
||||||
|
assertCanCreateContainer()
|
||||||
|
|
||||||
|
let container = SingleValueContainer(codingPath: self.codingPath, userInfo: self.userInfo)
|
||||||
|
self.container = container
|
||||||
|
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension _MessagePackEncoder {
|
||||||
|
final class SingleValueContainer {
|
||||||
|
private var storage: Data = Data()
|
||||||
|
|
||||||
|
fileprivate var canEncodeNewValue = true
|
||||||
|
fileprivate func checkCanEncode(value: Any?) throws {
|
||||||
|
guard self.canEncodeNewValue else {
|
||||||
|
let context = EncodingError.Context(codingPath: self.codingPath, debugDescription: "Attempt to encode value through single value container when previously value already encoded.")
|
||||||
|
throw EncodingError.invalidValue(value as Any, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var codingPath: [CodingKey]
|
||||||
|
var userInfo: [CodingUserInfoKey: Any]
|
||||||
|
|
||||||
|
init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _MessagePackEncoder.SingleValueContainer: SingleValueEncodingContainer {
|
||||||
|
func encodeNil() throws {
|
||||||
|
try checkCanEncode(value: nil)
|
||||||
|
defer { self.canEncodeNewValue = false }
|
||||||
|
|
||||||
|
self.storage.append(0xc0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: Bool) throws {
|
||||||
|
try checkCanEncode(value: nil)
|
||||||
|
defer { self.canEncodeNewValue = false }
|
||||||
|
|
||||||
|
switch value {
|
||||||
|
case false:
|
||||||
|
self.storage.append(0xc2)
|
||||||
|
case true:
|
||||||
|
self.storage.append(0xc3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: String) throws {
|
||||||
|
try checkCanEncode(value: value)
|
||||||
|
defer { self.canEncodeNewValue = false }
|
||||||
|
|
||||||
|
guard let data = value.data(using: .utf8) else {
|
||||||
|
let context = EncodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot encode string using UTF-8 encoding.")
|
||||||
|
throw EncodingError.invalidValue(value, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
let length = data.count
|
||||||
|
if let uint8 = UInt8(exactly: length) {
|
||||||
|
if (uint8 <= 31) {
|
||||||
|
self.storage.append(0xa0 + uint8)
|
||||||
|
} else {
|
||||||
|
self.storage.append(0xd9)
|
||||||
|
self.storage.append(contentsOf: uint8.bytes)
|
||||||
|
}
|
||||||
|
} else if let uint16 = UInt16(exactly: length) {
|
||||||
|
self.storage.append(0xda)
|
||||||
|
self.storage.append(contentsOf: uint16.bytes)
|
||||||
|
} else if let uint32 = UInt32(exactly: length) {
|
||||||
|
self.storage.append(0xdb)
|
||||||
|
self.storage.append(contentsOf: uint32.bytes)
|
||||||
|
} else {
|
||||||
|
let context = EncodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot encode string with length \(length).")
|
||||||
|
throw EncodingError.invalidValue(value, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.storage.append(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: Double) throws {
|
||||||
|
try checkCanEncode(value: value)
|
||||||
|
defer { self.canEncodeNewValue = false }
|
||||||
|
|
||||||
|
self.storage.append(0xcb)
|
||||||
|
self.storage.append(contentsOf: value.bitPattern.bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: Float) throws {
|
||||||
|
try checkCanEncode(value: value)
|
||||||
|
defer { self.canEncodeNewValue = false }
|
||||||
|
|
||||||
|
self.storage.append(0xca)
|
||||||
|
self.storage.append(contentsOf: value.bitPattern.bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode<T>(_ value: T) throws where T : BinaryInteger & Encodable {
|
||||||
|
try checkCanEncode(value: value)
|
||||||
|
defer { self.canEncodeNewValue = false }
|
||||||
|
|
||||||
|
if value < 0 {
|
||||||
|
if let int8 = Int8(exactly: value) {
|
||||||
|
return try encode(int8)
|
||||||
|
} else if let int16 = Int16(exactly: value) {
|
||||||
|
return try encode(int16)
|
||||||
|
} else if let int32 = Int32(exactly: value) {
|
||||||
|
return try encode(int32)
|
||||||
|
} else if let int64 = Int64(exactly: value) {
|
||||||
|
return try encode(int64)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let uint8 = UInt8(exactly: value) {
|
||||||
|
return try encode(uint8)
|
||||||
|
} else if let uint16 = UInt16(exactly: value) {
|
||||||
|
return try encode(uint16)
|
||||||
|
} else if let uint32 = UInt32(exactly: value) {
|
||||||
|
return try encode(uint32)
|
||||||
|
} else if let uint64 = UInt64(exactly: value) {
|
||||||
|
return try encode(uint64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = EncodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot encode integer \(value).")
|
||||||
|
throw EncodingError.invalidValue(value, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: Int8) throws {
|
||||||
|
try checkCanEncode(value: value)
|
||||||
|
defer { self.canEncodeNewValue = false }
|
||||||
|
|
||||||
|
if (value >= 0 && value <= 127) {
|
||||||
|
self.storage.append(UInt8(value))
|
||||||
|
} else if (value < 0 && value >= -31) {
|
||||||
|
self.storage.append(0xe0 + (0x1f & UInt8(truncatingIfNeeded: value)))
|
||||||
|
} else {
|
||||||
|
self.storage.append(0xd0)
|
||||||
|
self.storage.append(contentsOf: value.bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: Int16) throws {
|
||||||
|
try checkCanEncode(value: value)
|
||||||
|
defer { self.canEncodeNewValue = false }
|
||||||
|
|
||||||
|
self.storage.append(0xd1)
|
||||||
|
self.storage.append(contentsOf: value.bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: Int32) throws {
|
||||||
|
try checkCanEncode(value: value)
|
||||||
|
defer { self.canEncodeNewValue = false }
|
||||||
|
|
||||||
|
self.storage.append(0xd2)
|
||||||
|
self.storage.append(contentsOf: value.bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: Int64) throws {
|
||||||
|
try checkCanEncode(value: value)
|
||||||
|
defer { self.canEncodeNewValue = false }
|
||||||
|
|
||||||
|
self.storage.append(0xd3)
|
||||||
|
self.storage.append(contentsOf: value.bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: UInt8) throws {
|
||||||
|
try checkCanEncode(value: value)
|
||||||
|
defer { self.canEncodeNewValue = false }
|
||||||
|
|
||||||
|
if (value <= 127) {
|
||||||
|
self.storage.append(value)
|
||||||
|
} else {
|
||||||
|
self.storage.append(0xcc)
|
||||||
|
self.storage.append(contentsOf: value.bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: UInt16) throws {
|
||||||
|
try checkCanEncode(value: value)
|
||||||
|
defer { self.canEncodeNewValue = false }
|
||||||
|
|
||||||
|
self.storage.append(0xcd)
|
||||||
|
self.storage.append(contentsOf: value.bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: UInt32) throws {
|
||||||
|
try checkCanEncode(value: value)
|
||||||
|
defer { self.canEncodeNewValue = false }
|
||||||
|
|
||||||
|
self.storage.append(0xce)
|
||||||
|
self.storage.append(contentsOf: value.bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: UInt64) throws {
|
||||||
|
try checkCanEncode(value: value)
|
||||||
|
defer { self.canEncodeNewValue = false }
|
||||||
|
|
||||||
|
self.storage.append(0xcf)
|
||||||
|
self.storage.append(contentsOf: value.bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: Date) throws {
|
||||||
|
try checkCanEncode(value: value)
|
||||||
|
defer { self.canEncodeNewValue = false }
|
||||||
|
|
||||||
|
let timeInterval = value.timeIntervalSince1970
|
||||||
|
let (integral, fractional) = modf(timeInterval)
|
||||||
|
|
||||||
|
let seconds = Int64(integral)
|
||||||
|
let nanoseconds = UInt32(fractional * Double(NSEC_PER_SEC))
|
||||||
|
|
||||||
|
if seconds < 0 || seconds > UInt32.max {
|
||||||
|
self.storage.append(0xc7)
|
||||||
|
self.storage.append(0x0C)
|
||||||
|
self.storage.append(0xFF)
|
||||||
|
self.storage.append(contentsOf: nanoseconds.bytes)
|
||||||
|
self.storage.append(contentsOf: seconds.bytes)
|
||||||
|
} else if nanoseconds > 0 {
|
||||||
|
self.storage.append(0xd7)
|
||||||
|
self.storage.append(0xFF)
|
||||||
|
self.storage.append(contentsOf: ((UInt64(nanoseconds) << 34) + UInt64(seconds)).bytes)
|
||||||
|
} else {
|
||||||
|
self.storage.append(0xd6)
|
||||||
|
self.storage.append(0xFF)
|
||||||
|
self.storage.append(contentsOf: UInt32(seconds).bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: Data) throws {
|
||||||
|
let length = value.count
|
||||||
|
if let uint8 = UInt8(exactly: length) {
|
||||||
|
self.storage.append(0xc4)
|
||||||
|
self.storage.append(uint8)
|
||||||
|
self.storage.append(value)
|
||||||
|
} else if let uint16 = UInt16(exactly: length) {
|
||||||
|
self.storage.append(0xc5)
|
||||||
|
self.storage.append(contentsOf: uint16.bytes)
|
||||||
|
self.storage.append(value)
|
||||||
|
} else if let uint32 = UInt32(exactly: length) {
|
||||||
|
self.storage.append(0xc6)
|
||||||
|
self.storage.append(contentsOf: uint32.bytes)
|
||||||
|
self.storage.append(value)
|
||||||
|
} else {
|
||||||
|
let context = EncodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot encode data of length \(value.count).")
|
||||||
|
throw EncodingError.invalidValue(value, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode<T>(_ value: T) throws where T : Encodable {
|
||||||
|
try checkCanEncode(value: value)
|
||||||
|
defer { self.canEncodeNewValue = false }
|
||||||
|
|
||||||
|
switch value {
|
||||||
|
case let data as Data:
|
||||||
|
try self.encode(data)
|
||||||
|
case let date as Date:
|
||||||
|
try self.encode(date)
|
||||||
|
default:
|
||||||
|
let encoder = _MessagePackEncoder()
|
||||||
|
try value.encode(to: encoder)
|
||||||
|
self.storage.append(encoder.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _MessagePackEncoder.SingleValueContainer: _MessagePackEncodingContainer {
|
||||||
|
var data: Data {
|
||||||
|
return storage
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension _MessagePackEncoder {
|
||||||
|
final class UnkeyedContainer {
|
||||||
|
private var storage: [_MessagePackEncodingContainer] = []
|
||||||
|
|
||||||
|
var count: Int {
|
||||||
|
return storage.count
|
||||||
|
}
|
||||||
|
|
||||||
|
var codingPath: [CodingKey]
|
||||||
|
|
||||||
|
var nestedCodingPath: [CodingKey] {
|
||||||
|
return self.codingPath + [AnyCodingKey(intValue: self.count)!]
|
||||||
|
}
|
||||||
|
|
||||||
|
var userInfo: [CodingUserInfoKey: Any]
|
||||||
|
|
||||||
|
init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _MessagePackEncoder.UnkeyedContainer: UnkeyedEncodingContainer {
|
||||||
|
func encodeNil() throws {
|
||||||
|
var container = self.nestedSingleValueContainer()
|
||||||
|
try container.encodeNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode<T>(_ value: T) throws where T : Encodable {
|
||||||
|
var container = self.nestedSingleValueContainer()
|
||||||
|
try container.encode(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func nestedSingleValueContainer() -> SingleValueEncodingContainer {
|
||||||
|
let container = _MessagePackEncoder.SingleValueContainer(codingPath: self.nestedCodingPath, userInfo: self.userInfo)
|
||||||
|
self.storage.append(container)
|
||||||
|
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||||
|
let container = _MessagePackEncoder.KeyedContainer<NestedKey>(codingPath: self.nestedCodingPath, userInfo: self.userInfo)
|
||||||
|
self.storage.append(container)
|
||||||
|
|
||||||
|
return KeyedEncodingContainer(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
|
||||||
|
let container = _MessagePackEncoder.UnkeyedContainer(codingPath: self.nestedCodingPath, userInfo: self.userInfo)
|
||||||
|
self.storage.append(container)
|
||||||
|
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
func superEncoder() -> Encoder {
|
||||||
|
fatalError("Unimplemented") // FIXME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _MessagePackEncoder.UnkeyedContainer: _MessagePackEncodingContainer {
|
||||||
|
var data: Data {
|
||||||
|
var data = Data()
|
||||||
|
|
||||||
|
let length = storage.count
|
||||||
|
if let uint16 = UInt16(exactly: length) {
|
||||||
|
if uint16 <= 15 {
|
||||||
|
data.append(UInt8(0x90 + uint16))
|
||||||
|
} else {
|
||||||
|
data.append(0xdc)
|
||||||
|
data.append(contentsOf: uint16.bytes)
|
||||||
|
}
|
||||||
|
} else if let uint32 = UInt32(exactly: length) {
|
||||||
|
data.append(0xdd)
|
||||||
|
data.append(contentsOf: uint32.bytes)
|
||||||
|
} else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
for container in storage {
|
||||||
|
data.append(container.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
extension FixedWidthInteger {
|
||||||
|
init(bytes: [UInt8]) {
|
||||||
|
self = bytes.withUnsafeBufferPointer {
|
||||||
|
$0.baseAddress!.withMemoryRebound(to: Self.self, capacity: 1) {
|
||||||
|
$0.pointee
|
||||||
|
}
|
||||||
|
}.bigEndian
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes: [UInt8] {
|
||||||
|
let capacity = MemoryLayout<Self>.size
|
||||||
|
var mutableValue = self.bigEndian
|
||||||
|
return withUnsafePointer(to: &mutableValue) {
|
||||||
|
return $0.withMemoryRebound(to: UInt8.self, capacity: capacity) {
|
||||||
|
return Array(UnsafeBufferPointer(start: $0, count: capacity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
8
lib/MessagePack/Tests/LinuxMain.swift
Normal file
8
lib/MessagePack/Tests/LinuxMain.swift
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import MessagePackTests
|
||||||
|
|
||||||
|
XCTMain([
|
||||||
|
testCase(MessagePackDecodingTests.allTests),
|
||||||
|
testCase(MessagePackEncodingTests.allTests),
|
||||||
|
testCase(MessagePackRoundTripTests.allTests),
|
||||||
|
])
|
||||||
63
lib/MessagePack/Tests/MessagePackTests/Airport.swift
Normal file
63
lib/MessagePack/Tests/MessagePackTests/Airport.swift
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
struct Airport: Codable, Equatable {
|
||||||
|
let name: String
|
||||||
|
let iata: String
|
||||||
|
let icao: String
|
||||||
|
let coordinates: [Double]
|
||||||
|
|
||||||
|
struct Runway: Codable, Equatable {
|
||||||
|
enum Surface: String, Codable, Equatable {
|
||||||
|
case rigid, flexible, gravel, sealed, unpaved, other
|
||||||
|
}
|
||||||
|
|
||||||
|
let direction: String
|
||||||
|
let distance: Int
|
||||||
|
let surface: Surface
|
||||||
|
}
|
||||||
|
|
||||||
|
let runways: [Runway]
|
||||||
|
|
||||||
|
let instrumentApproachProcedures: [String]
|
||||||
|
|
||||||
|
static var example: Airport {
|
||||||
|
return Airport(
|
||||||
|
name: "Portland International Airport",
|
||||||
|
iata: "PDX",
|
||||||
|
icao: "KPDX",
|
||||||
|
coordinates: [-122.5975,
|
||||||
|
45.5886111111111],
|
||||||
|
runways: [
|
||||||
|
Airport.Runway(
|
||||||
|
direction: "3/21",
|
||||||
|
distance: 1829,
|
||||||
|
surface: .flexible
|
||||||
|
)
|
||||||
|
],
|
||||||
|
instrumentApproachProcedures: [
|
||||||
|
"HI-ILS OR LOC RWY 28",
|
||||||
|
"HI-ILS OR LOC/DME RWY 10",
|
||||||
|
"ILS OR LOC RWY 10L",
|
||||||
|
"ILS OR LOC RWY 10R",
|
||||||
|
"ILS OR LOC RWY 28L",
|
||||||
|
"ILS OR LOC RWY 28R",
|
||||||
|
"ILS RWY 10R (SA CAT I)",
|
||||||
|
"ILS RWY 10R (CAT II - III)",
|
||||||
|
"RNAV (RNP) Y RWY 28L",
|
||||||
|
"RNAV (RNP) Y RWY 28R",
|
||||||
|
"RNAV (RNP) Z RWY 10L",
|
||||||
|
"RNAV (RNP) Z RWY 10R",
|
||||||
|
"RNAV (RNP) Z RWY 28L",
|
||||||
|
"RNAV (RNP) Z RWY 28R",
|
||||||
|
"RNAV (GPS) X RWY 28L",
|
||||||
|
"RNAV (GPS) X RWY 28R",
|
||||||
|
"RNAV (GPS) Y RWY 10L",
|
||||||
|
"RNAV (GPS) Y RWY 10R",
|
||||||
|
"LOC/DME RWY 21",
|
||||||
|
"VOR-A",
|
||||||
|
"HI-TACAN RWY 10",
|
||||||
|
"TACAN RWY 28",
|
||||||
|
"COLUMBIA VISUAL RWY 10L/",
|
||||||
|
"MILL VISUAL RWY 28L/R"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import MessagePack
|
||||||
|
|
||||||
|
class MessagePackDecodingTests: XCTestCase {
|
||||||
|
var decoder: MessagePackDecoder!
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
self.decoder = MessagePackDecoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertTypeMismatch<T>(_ expression: @autoclosure () throws -> T,
|
||||||
|
_ message: @autoclosure () -> String = "",
|
||||||
|
file: StaticString = #file,
|
||||||
|
line: UInt = #line) -> Any.Type? {
|
||||||
|
var error: Error?
|
||||||
|
XCTAssertThrowsError(expression, message,
|
||||||
|
file: file, line: line) {
|
||||||
|
error = $0
|
||||||
|
}
|
||||||
|
guard case .typeMismatch(let type, _) = error as? DecodingError else {
|
||||||
|
XCTFail(file: file, line: line)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeNil() {
|
||||||
|
let data = Data(bytes: [0xC0])
|
||||||
|
let value = try! decoder.decode(Int?.self, from: data)
|
||||||
|
XCTAssertNil(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeFalse() {
|
||||||
|
let data = Data(bytes: [0xc2])
|
||||||
|
let value = try! decoder.decode(Bool.self, from: data)
|
||||||
|
XCTAssertEqual(value, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeTrue() {
|
||||||
|
let data = Data(bytes: [0xc3])
|
||||||
|
let value = try! decoder.decode(Bool.self, from: data)
|
||||||
|
XCTAssertEqual(value, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeInt() {
|
||||||
|
let data = Data(bytes: [0x2A])
|
||||||
|
let value = try! decoder.decode(Int.self, from: data)
|
||||||
|
XCTAssertEqual(value, 42)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeNegativeInt() {
|
||||||
|
let data = Data(bytes: [0xFF])
|
||||||
|
let value = try! decoder.decode(Int.self, from: data)
|
||||||
|
XCTAssertEqual(value, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeUInt() {
|
||||||
|
let data = Data(bytes: [0xCC, 0x80])
|
||||||
|
let value = try! decoder.decode(Int.self, from: data)
|
||||||
|
XCTAssertEqual(value, 128)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeFloat() {
|
||||||
|
let data = Data(bytes: [0xCA, 0x40, 0x48, 0xF5, 0xC3])
|
||||||
|
let value = try! decoder.decode(Float.self, from: data)
|
||||||
|
XCTAssertEqual(value, 3.14)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeFloatToDouble() {
|
||||||
|
let data = Data(bytes: [0xCA, 0x40, 0x48, 0xF5, 0xC3])
|
||||||
|
let type = assertTypeMismatch(try decoder.decode(Double.self, from: data))
|
||||||
|
XCTAssertTrue(type is Double.Type)
|
||||||
|
decoder.nonMatchingFloatDecodingStrategy = .cast
|
||||||
|
let value = try! decoder.decode(Double.self, from: data)
|
||||||
|
XCTAssertEqual(value, 3.14, accuracy: 1e-6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeDouble() {
|
||||||
|
let data = Data(bytes: [0xCB, 0x40, 0x09, 0x21, 0xF9, 0xF0, 0x1B, 0x86, 0x6E])
|
||||||
|
let value = try! decoder.decode(Double.self, from: data)
|
||||||
|
XCTAssertEqual(value, 3.14159)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeDoubleToFloat() {
|
||||||
|
let data = Data(bytes: [0xCB, 0x40, 0x09, 0x21, 0xF9, 0xF0, 0x1B, 0x86, 0x6E])
|
||||||
|
let type = assertTypeMismatch(try decoder.decode(Float.self, from: data))
|
||||||
|
XCTAssertTrue(type is Float.Type)
|
||||||
|
decoder.nonMatchingFloatDecodingStrategy = .cast
|
||||||
|
let value = try! decoder.decode(Float.self, from: data)
|
||||||
|
XCTAssertEqual(value, 3.14159)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeFixedArray() {
|
||||||
|
let data = Data(bytes: [0x93, 0x01, 0x02, 0x03])
|
||||||
|
let value = try! decoder.decode([Int].self, from: data)
|
||||||
|
XCTAssertEqual(value, [1, 2, 3])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeVariableArray() {
|
||||||
|
let data = Data(bytes: [0xdc] + [0x00, 0x10] + Array(0x01...0x10))
|
||||||
|
let value = try! decoder.decode([Int].self, from: data)
|
||||||
|
XCTAssertEqual(value, Array(1...16))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeFixedDictionary() {
|
||||||
|
let data = Data(bytes: [0x83, 0xA1, 0x62, 0x02, 0xA1, 0x61, 0x01, 0xA1, 0x63, 0x03])
|
||||||
|
let value = try! decoder.decode([String: Int].self, from: data)
|
||||||
|
XCTAssertEqual(value, ["a": 1, "b": 2, "c": 3])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeData() {
|
||||||
|
let data = Data(bytes: [0xC4, 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F])
|
||||||
|
let value = try! decoder.decode(Data.self, from: data)
|
||||||
|
XCTAssertEqual(value, "hello".data(using: .utf8))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeDate() {
|
||||||
|
let data = Data(bytes: [0xD6, 0xFF, 0x00, 0x00, 0x00, 0x01])
|
||||||
|
let date = Date(timeIntervalSince1970: 1)
|
||||||
|
let value = try! decoder.decode(Date.self, from: data)
|
||||||
|
XCTAssertEqual(value, date)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeDistantPast() {
|
||||||
|
let data = Data(bytes: [0xC7, 0x0C, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xF1, 0x88, 0x6B, 0x66, 0x00])
|
||||||
|
let date = Date.distantPast
|
||||||
|
let value = try! decoder.decode(Date.self, from: data)
|
||||||
|
XCTAssertEqual(value, date)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeDistantFuture() {
|
||||||
|
let data = Data(bytes: [0xC7, 0x0C, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0xEC, 0x31, 0x88, 0x00])
|
||||||
|
let date = Date.distantFuture
|
||||||
|
let value = try! decoder.decode(Date.self, from: data)
|
||||||
|
XCTAssertEqual(value, date)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeArrayWithDate() {
|
||||||
|
let data = Data(bytes: [0x91, 0xD6, 0xFF, 0x00, 0x00, 0x00, 0x01])
|
||||||
|
let date = Date(timeIntervalSince1970: 1)
|
||||||
|
let value = try! decoder.decode([Date].self, from: data)
|
||||||
|
XCTAssertEqual(value, [date])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeDictionaryWithDate() {
|
||||||
|
let data = Data(bytes: [0x81, 0xA1, 0x31, 0xD6, 0xFF, 0x00, 0x00, 0x00, 0x01])
|
||||||
|
let date = Date(timeIntervalSince1970: 1)
|
||||||
|
let value = try! decoder.decode([String: Date].self, from: data)
|
||||||
|
XCTAssertEqual(value, ["1": date])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeBv() {
|
||||||
|
let b64 = "lK50ZXN0aW5nIHN0cmluZyeSpnF3ZXF3ZagxMjNpY29uc5OU2SRlMTZkYTYwMi0zMjE1LTRiZDYtYjY5MC00Y2Q4NmEwZmU3NjSoQ2lwaGVyIDEBk61jaXBodXNlcm5hbWUxrWFkZmFmZHcyMzQxMzGSkblodHRwczovL3d3dy5nb29nbGUuY29tLmFykbVodHRwczovL3d3dy5hcHBsZS5jb22U2SRhNjExMWU2Ny1hMTMwLTRiM2ItODM5NS0xZjIzMDFjNjk3ZjeoQ2lwaGVyIDIBk6g0MzEzMjEzMatqbGpsbHl1bHVpecCU2SRiOGIwODM3MC0xNGU0LTQzZmUtYjBkOS04ZjJlMDlmODJkYzWoQ2lwaGVyIDMBk6twaW9waW9waXBpb6x6eGN6eHZ6eHZ4enaSkbdodHRwczovL3d3dy52aXNhLmNvbS5hcpG1aHR0cHM6Ly93d3cuZG9ja3MuY29t" // array mode with envData and ciphers
|
||||||
|
|
||||||
|
// let b64 = "hKFirnRlc3Rpbmcgc3RyaW5noWMnp2VudkRhdGGCpGJhc2WmcXdlcXdlpWljb25zqDEyM2ljb25zp2NpcGhlcnOThKJpZNkkMDA4YmE0NDctZjU0Mi00OWVjLWJjYTktMDMzZTQ2OTU0YTBipG5hbWWoQ2lwaGVyIDGkdHlwZQGlbG9naW6DqHVzZXJuYW1lrWNpcGh1c2VybmFtZTGkdG90cK1hZGZhZmR3MjM0MTMxpHVyaXOSgaN1cmm5aHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS5hcoGjdXJptWh0dHBzOi8vd3d3LmFwcGxlLmNvbYSiaWTZJDQ1ZTBhODJiLTgyZGQtNDJiZi05ODhhLTAyYTkyNGM4Yzg5M6RuYW1lqENpcGhlciAypHR5cGUBpWxvZ2lug6h1c2VybmFtZag0MzEzMjEzMaR0b3Rwq2psamxseXVsdWl5pHVyaXPAhKJpZNkkZTBjZWU5NDEtZDI1Ni00MjdiLWJkNWUtNDMxMmMwN2U1NDI5pG5hbWWoQ2lwaGVyIDOkdHlwZQGlbG9naW6DqHVzZXJuYW1lq3Bpb3Bpb3BpcGlvpHRvdHCsenhjenh2enh2eHp2pHVyaXOSgaN1cmm3aHR0cHM6Ly93d3cudmlzYS5jb20uYXKBo3VyabVodHRwczovL3d3dy5kb2Nrcy5jb20=" // dict mode with envData and ciphers
|
||||||
|
|
||||||
|
do {
|
||||||
|
if let d = Data(base64Encoded: b64) {
|
||||||
|
let decoder = MessagePackDecoder()
|
||||||
|
decoder.userInfo[MessagePackDecoder.dataSpecKey] = DataSpecBuilder()
|
||||||
|
.append("b")
|
||||||
|
.append("c")
|
||||||
|
.appendObj("envData", DataSpecBuilder()
|
||||||
|
.append("base")
|
||||||
|
.append("icons")
|
||||||
|
.build())
|
||||||
|
.appendArray("ciphers", DataSpecBuilder()
|
||||||
|
.append("id")
|
||||||
|
.append("name")
|
||||||
|
.append("type")
|
||||||
|
.appendObj("login", DataSpecBuilder()
|
||||||
|
.append("username")
|
||||||
|
.append("totp")
|
||||||
|
.appendArray("uris", DataSpecBuilder()
|
||||||
|
.append("uri")
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
let codTest = try decoder.decode(CodableTest.self, from: d)
|
||||||
|
|
||||||
|
XCTAssertEqual(codTest.b, "testing string")
|
||||||
|
XCTAssertEqual(codTest.envData.base, "qweqwe")
|
||||||
|
XCTAssertEqual(codTest.envData.icons, "123icons")
|
||||||
|
XCTAssertTrue(codTest.ciphers!.count > 1)
|
||||||
|
} else {
|
||||||
|
XCTAssertEqual(1, 0)
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
XCTFail("E: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var allTests = [
|
||||||
|
("testDecodeNil", testDecodeNil),
|
||||||
|
("testDecodeFalse", testDecodeFalse),
|
||||||
|
("testDecodeTrue", testDecodeTrue),
|
||||||
|
("testDecodeInt", testDecodeInt),
|
||||||
|
("testDecodeUInt", testDecodeUInt),
|
||||||
|
("testDecodeFloat", testDecodeFloat),
|
||||||
|
("testDecodeFloatToDouble", testDecodeFloatToDouble),
|
||||||
|
("testDecodeDouble", testDecodeDouble),
|
||||||
|
("testDecodeDoubleToFloat", testDecodeDoubleToFloat),
|
||||||
|
("testDecodeFixedArray", testDecodeFixedArray),
|
||||||
|
("testDecodeFixedDictionary", testDecodeFixedDictionary),
|
||||||
|
("testDecodeData", testDecodeData),
|
||||||
|
("testDecodeDistantPast", testDecodeDistantPast),
|
||||||
|
("testDecodeDistantFuture", testDecodeDistantFuture),
|
||||||
|
("testDecodeArrayWithDate", testDecodeArrayWithDate),
|
||||||
|
("testDecodeDictionaryWithDate", testDecodeDictionaryWithDate),
|
||||||
|
("testDecodeBv", testDecodeBv)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CodableTest : Codable {
|
||||||
|
enum CodingKeys: Int, CodingKey {
|
||||||
|
case b
|
||||||
|
case c
|
||||||
|
case envData
|
||||||
|
case ciphers
|
||||||
|
}
|
||||||
|
|
||||||
|
var b: String
|
||||||
|
var c: Int
|
||||||
|
var envData: EnvironmentUrlDataDto
|
||||||
|
var ciphers: [Cipher]?
|
||||||
|
|
||||||
|
func printt() {
|
||||||
|
print("B: \(b)")
|
||||||
|
print("C: \(c)")
|
||||||
|
print("ENVDATA")
|
||||||
|
envData.printt()
|
||||||
|
|
||||||
|
if let cs = ciphers {
|
||||||
|
print("CIPHERS")
|
||||||
|
for c in cs {
|
||||||
|
c.printt()
|
||||||
|
print("----------------------------")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print("###########################")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EnvironmentUrlDataDto : Codable {
|
||||||
|
var base: String?
|
||||||
|
var icons: String?
|
||||||
|
|
||||||
|
func printt() {
|
||||||
|
print("Base: \(base ?? "")")
|
||||||
|
print("Icons: \(icons ?? "")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Cipher:Identifiable,Codable{
|
||||||
|
enum CodingKeys: Int, CodingKey {
|
||||||
|
case id
|
||||||
|
case name
|
||||||
|
case login
|
||||||
|
}
|
||||||
|
|
||||||
|
var id:String
|
||||||
|
var name:String?
|
||||||
|
var userId:String?
|
||||||
|
var login:Login
|
||||||
|
|
||||||
|
func printt() {
|
||||||
|
print("id: \(id)")
|
||||||
|
print("name: \(name ?? "")")
|
||||||
|
print("LOGIN")
|
||||||
|
login.printt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Login:Codable{
|
||||||
|
var username:String?
|
||||||
|
var totp:String?
|
||||||
|
var uris:[LoginUri]?
|
||||||
|
|
||||||
|
func printt() {
|
||||||
|
print("username: \(username ?? "")")
|
||||||
|
print("totp: \(totp ?? "")")
|
||||||
|
print("URIS")
|
||||||
|
if let us = uris {
|
||||||
|
for u in us {
|
||||||
|
u.printt()
|
||||||
|
print("----------------------------")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LoginUri:Codable{
|
||||||
|
var uri:String?
|
||||||
|
|
||||||
|
func printt() {
|
||||||
|
print("Uri: \(uri ?? "")")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import MessagePack
|
||||||
|
|
||||||
|
class MessagePackEncodingTests: XCTestCase {
|
||||||
|
var encoder: MessagePackEncoder!
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
self.encoder = MessagePackEncoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeNil() {
|
||||||
|
let value = try! encoder.encode(nil as Int?)
|
||||||
|
XCTAssertEqual(value, Data(bytes: [0xc0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeFalse() {
|
||||||
|
let value = try! encoder.encode(false)
|
||||||
|
XCTAssertEqual(value, Data(bytes: [0xc2]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeTrue() {
|
||||||
|
let value = try! encoder.encode(true)
|
||||||
|
XCTAssertEqual(value, Data(bytes: [0xc3]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeInt() {
|
||||||
|
let value = try! encoder.encode(42 as Int)
|
||||||
|
XCTAssertEqual(value, Data(bytes: [0x2A]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeUInt() {
|
||||||
|
let value = try! encoder.encode(128 as UInt)
|
||||||
|
XCTAssertEqual(value, Data(bytes: [0xCC, 0x80]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeFloat() {
|
||||||
|
let value = try! encoder.encode(3.14 as Float)
|
||||||
|
XCTAssertEqual(value, Data(bytes: [0xCA, 0x40, 0x48, 0xF5, 0xC3]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeDouble() {
|
||||||
|
let value = try! encoder.encode(3.14159 as Double)
|
||||||
|
XCTAssertEqual(value, Data(bytes: [0xCB, 0x40, 0x09, 0x21, 0xF9, 0xF0, 0x1B, 0x86, 0x6E]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeString() {
|
||||||
|
let value = try! encoder.encode("hello")
|
||||||
|
XCTAssertEqual(value, Data(bytes: [0xA5, 0x68, 0x65, 0x6C, 0x6C, 0x6F]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeFixedArray() {
|
||||||
|
let value = try! encoder.encode([1, 2, 3])
|
||||||
|
XCTAssertEqual(value, Data(bytes: [0x93, 0x01, 0x02, 0x03]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeVariableArray() {
|
||||||
|
let value = try! encoder.encode(Array(1...16))
|
||||||
|
XCTAssertEqual(value, Data(bytes: [0xdc] + [0x00, 0x10] + Array(0x01...0x10)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeFixedDictionary() {
|
||||||
|
let value = try! encoder.encode(["a": 1])
|
||||||
|
XCTAssertEqual(value, Data(bytes: [0x81, 0xA1, 0x61, 0x01]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeVariableDictionary() {
|
||||||
|
let letters = "abcdefghijklmnopqrstuvwxyz".unicodeScalars
|
||||||
|
let dictionary = Dictionary(uniqueKeysWithValues: zip(letters.map { String($0) }, 1...26))
|
||||||
|
let value = try! encoder.encode(dictionary)
|
||||||
|
XCTAssertEqual(value.count, 81)
|
||||||
|
XCTAssert(value.starts(with: [0xde] + [0x00, 0x1A]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeData() {
|
||||||
|
let data = "hello".data(using: .utf8)
|
||||||
|
let value = try! encoder.encode(data)
|
||||||
|
XCTAssertEqual(value, Data(bytes: [0xC4, 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeDate() {
|
||||||
|
let date = Date(timeIntervalSince1970: 1)
|
||||||
|
let value = try! encoder.encode(date)
|
||||||
|
XCTAssertEqual(value, Data(bytes: [0xD6, 0xFF, 0x00, 0x00, 0x00, 0x01]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeDistantPast() {
|
||||||
|
let date = Date.distantPast
|
||||||
|
let value = try! encoder.encode(date)
|
||||||
|
XCTAssertEqual(value, Data(bytes: [0xC7, 0x0C, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xF1, 0x88, 0x6B, 0x66, 0x00]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeDistantFuture() {
|
||||||
|
let date = Date.distantFuture
|
||||||
|
let value = try! encoder.encode(date)
|
||||||
|
XCTAssertEqual(value, Data(bytes: [0xC7, 0x0C, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0xEC, 0x31, 0x88, 0x00]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeArrayWithDate() {
|
||||||
|
let date = Date(timeIntervalSince1970: 1)
|
||||||
|
let value = try! encoder.encode([date])
|
||||||
|
XCTAssertEqual(value, Data(bytes: [0x91, 0xD6, 0xFF, 0x00, 0x00, 0x00, 0x01]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeDictionaryWithDate() {
|
||||||
|
let date = Date(timeIntervalSince1970: 1)
|
||||||
|
let value = try! encoder.encode(["1": date])
|
||||||
|
XCTAssertEqual(value, Data(bytes: [0x81, 0xA1, 0x31, 0xD6, 0xFF, 0x00, 0x00, 0x00, 0x01]))
|
||||||
|
}
|
||||||
|
|
||||||
|
static var allTests = [
|
||||||
|
("testEncodeFalse", testEncodeFalse),
|
||||||
|
("testEncodeTrue", testEncodeTrue),
|
||||||
|
("testEncodeInt", testEncodeInt),
|
||||||
|
("testEncodeUInt", testEncodeUInt),
|
||||||
|
("testEncodeFloat", testEncodeFloat),
|
||||||
|
("testEncodeDouble", testEncodeDouble),
|
||||||
|
("testEncodeFixedArray", testEncodeFixedArray),
|
||||||
|
("testEncodeVariableArray", testEncodeVariableArray),
|
||||||
|
("testEncodeFixedDictionary", testEncodeFixedDictionary),
|
||||||
|
("testEncodeVariableDictionary", testEncodeVariableDictionary),
|
||||||
|
("testEncodeDate", testEncodeDate),
|
||||||
|
("testEncodeDistantPast", testEncodeDistantPast),
|
||||||
|
("testEncodeDistantFuture", testEncodeDistantFuture),
|
||||||
|
("testEncodeArrayWithDate", testEncodeArrayWithDate),
|
||||||
|
("testEncodeDictionaryWithDate", testEncodeDictionaryWithDate)
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import MessagePack
|
||||||
|
|
||||||
|
class MessagePackPerformanceTests: XCTestCase {
|
||||||
|
var encoder: MessagePackEncoder!
|
||||||
|
var decoder: MessagePackDecoder!
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
self.encoder = MessagePackEncoder()
|
||||||
|
self.decoder = MessagePackDecoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPerformance() {
|
||||||
|
let count = 100
|
||||||
|
let values = [Airport](repeating: .example, count: count)
|
||||||
|
|
||||||
|
self.measure {
|
||||||
|
let encoded = try! encoder.encode(values)
|
||||||
|
let decoded = try! decoder.decode([Airport].self, from: encoded)
|
||||||
|
XCTAssertEqual(decoded.count, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import MessagePack
|
||||||
|
|
||||||
|
class MessagePackRoundTripTests: XCTestCase {
|
||||||
|
var encoder: MessagePackEncoder!
|
||||||
|
var decoder: MessagePackDecoder!
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
self.encoder = MessagePackEncoder()
|
||||||
|
self.decoder = MessagePackDecoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRoundTripAirport() {
|
||||||
|
let value = Airport.example
|
||||||
|
let encoded = try! encoder.encode(value)
|
||||||
|
let decoded = try! decoder.decode(Airport.self, from: encoded)
|
||||||
|
|
||||||
|
XCTAssertEqual(value.name, decoded.name)
|
||||||
|
XCTAssertEqual(value.iata, decoded.iata)
|
||||||
|
XCTAssertEqual(value.icao, decoded.icao)
|
||||||
|
XCTAssertEqual(value.coordinates[0], decoded.coordinates[0], accuracy: 0.01)
|
||||||
|
XCTAssertEqual(value.coordinates[1], decoded.coordinates[1], accuracy: 0.01)
|
||||||
|
XCTAssertEqual(value.runways[0].direction, decoded.runways[0].direction)
|
||||||
|
XCTAssertEqual(value.runways[0].distance, decoded.runways[0].distance)
|
||||||
|
XCTAssertEqual(value.runways[0].surface, decoded.runways[0].surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRoundTripParachutePack() {
|
||||||
|
struct Parachute: Codable, Equatable {
|
||||||
|
enum Canopy: String, Codable, Equatable {
|
||||||
|
case round, cruciform, rogalloWing, annular, ramAir
|
||||||
|
}
|
||||||
|
|
||||||
|
let canpoy: Canopy
|
||||||
|
let surfaceArea: Double
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ParachutePack: Codable, Equatable {
|
||||||
|
let main: Parachute?
|
||||||
|
let reserve: Parachute?
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = ParachutePack(main: Parachute(canpoy: .ramAir, surfaceArea: 200), reserve: nil)
|
||||||
|
let encoded = try! encoder.encode(value)
|
||||||
|
let decoded = try! decoder.decode(ParachutePack.self, from: encoded)
|
||||||
|
|
||||||
|
XCTAssertEqual(value, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRoundTripArray() {
|
||||||
|
let count: UInt8 = 100
|
||||||
|
var bytes: [UInt8] = [0xdc, 0x00, count]
|
||||||
|
var encoded: [Int] = []
|
||||||
|
for n in 1...count {
|
||||||
|
bytes.append(n)
|
||||||
|
encoded.append(Int(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = Data(bytes: bytes)
|
||||||
|
let decoded = try! decoder.decode([Int].self, from: data)
|
||||||
|
XCTAssertEqual(encoded, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRoundTripDictionary() {
|
||||||
|
let (a, z): (UInt8, UInt8) = (0x61, 0x7a)
|
||||||
|
var bytes: [UInt8] = [0xde, 0x00, 0x1A]
|
||||||
|
var encoded: [String: Int] = [:]
|
||||||
|
for n in a...z {
|
||||||
|
bytes.append(contentsOf: [0xA1, n, n])
|
||||||
|
encoded[String(Unicode.Scalar(n))] = Int(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = Data(bytes: bytes)
|
||||||
|
let decoded = try! decoder.decode([String: Int].self, from: data)
|
||||||
|
XCTAssertEqual(encoded, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRoundTripDate() {
|
||||||
|
var bytes: [UInt8] = [0xD6, 0xFF]
|
||||||
|
|
||||||
|
let dateComponents = DateComponents(year: 2018, month: 4, day: 20)
|
||||||
|
let encoded = Calendar.current.date(from: dateComponents)!
|
||||||
|
|
||||||
|
let secondsSince1970 = UInt32(encoded.timeIntervalSince1970)
|
||||||
|
bytes.append(contentsOf: secondsSince1970.bytes)
|
||||||
|
|
||||||
|
let data = Data(bytes: bytes)
|
||||||
|
let decoded = try! decoder.decode(Date.self, from: data)
|
||||||
|
XCTAssertEqual(encoded, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRoundTripDateWithNanoseconds() {
|
||||||
|
let encoded = Date()
|
||||||
|
let data = try! self.encoder.encode(encoded)
|
||||||
|
let decoded = try! self.decoder.decode(Date.self, from: data)
|
||||||
|
XCTAssertEqual(encoded.timeIntervalSinceReferenceDate, decoded.timeIntervalSinceReferenceDate, accuracy: 0.0001)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var allTests = [
|
||||||
|
("testRoundTripAirport", testRoundTripAirport),
|
||||||
|
("testRoundTripArray", testRoundTripArray),
|
||||||
|
("testRoundTripDictionary", testRoundTripDictionary),
|
||||||
|
("testRoundTripDate", testRoundTripDate),
|
||||||
|
("testRoundTripDateWithNanoseconds", testRoundTripDateWithNanoseconds)
|
||||||
|
]
|
||||||
|
}
|
||||||
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>
|
||||||
169
package-lock.json
generated
169
package-lock.json
generated
@@ -8,7 +8,7 @@
|
|||||||
"name": "bitwarden-mobile",
|
"name": "bitwarden-mobile",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"gh-pages": "3.2.3"
|
"gh-pages": "^6.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/array-union": {
|
"node_modules/array-union": {
|
||||||
@@ -33,13 +33,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/async": {
|
"node_modules/async": {
|
||||||
"version": "2.6.3",
|
"version": "3.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
||||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"license": "MIT"
|
||||||
"lodash": "^4.17.14"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@@ -58,10 +56,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "2.20.3",
|
"version": "11.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
|
||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/commondir": {
|
"node_modules/commondir": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@@ -76,10 +78,11 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/email-addresses": {
|
"node_modules/email-addresses": {
|
||||||
"version": "3.1.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz",
|
||||||
"integrity": "sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg==",
|
"integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/escape-string-regexp": {
|
"node_modules/escape-string-regexp": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
@@ -147,17 +150,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "8.1.0",
|
"version": "11.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
|
||||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
"integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
"jsonfile": "^4.0.0",
|
"jsonfile": "^6.0.1",
|
||||||
"universalify": "^0.1.0"
|
"universalify": "^2.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6 <7 || >=8"
|
"node": ">=14.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fs.realpath": {
|
"node_modules/fs.realpath": {
|
||||||
@@ -167,17 +171,18 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/gh-pages": {
|
"node_modules/gh-pages": {
|
||||||
"version": "3.2.3",
|
"version": "6.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.1.1.tgz",
|
||||||
"integrity": "sha512-jA1PbapQ1jqzacECfjUaO9gV8uBgU6XNMV0oXLtfCX3haGLe5Atq8BxlrADhbD6/UdG9j6tZLWAkAybndOXTJg==",
|
"integrity": "sha512-upnohfjBwN5hBP9w2dPE7HO5JJTHzSGMV1JrLrHvNuqmjoYHg6TBrCcnEoorjG/e0ejbuvnwyKMdTyM40PEByw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async": "^2.6.1",
|
"async": "^3.2.4",
|
||||||
"commander": "^2.18.0",
|
"commander": "^11.0.0",
|
||||||
"email-addresses": "^3.0.1",
|
"email-addresses": "^5.0.0",
|
||||||
"filenamify": "^4.3.0",
|
"filenamify": "^4.3.0",
|
||||||
"find-cache-dir": "^3.3.1",
|
"find-cache-dir": "^3.3.1",
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^11.1.1",
|
||||||
"globby": "^6.1.0"
|
"globby": "^6.1.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -225,10 +230,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.8",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/inflight": {
|
"node_modules/inflight": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
@@ -247,10 +253,14 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/jsonfile": {
|
"node_modules/jsonfile": {
|
||||||
"version": "4.0.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||||
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"graceful-fs": "^4.1.6"
|
"graceful-fs": "^4.1.6"
|
||||||
}
|
}
|
||||||
@@ -267,12 +277,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lodash": {
|
|
||||||
"version": "4.17.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/make-dir": {
|
"node_modules/make-dir": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||||
@@ -448,12 +452,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/universalify": {
|
"node_modules/universalify": {
|
||||||
"version": "0.1.2",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 4.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wrappy": {
|
"node_modules/wrappy": {
|
||||||
@@ -480,13 +485,10 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"async": {
|
"async": {
|
||||||
"version": "2.6.3",
|
"version": "3.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
||||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"requires": {
|
|
||||||
"lodash": "^4.17.14"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@@ -505,9 +507,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"commander": {
|
"commander": {
|
||||||
"version": "2.20.3",
|
"version": "11.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
|
||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"commondir": {
|
"commondir": {
|
||||||
@@ -523,9 +525,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"email-addresses": {
|
"email-addresses": {
|
||||||
"version": "3.1.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz",
|
||||||
"integrity": "sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg==",
|
"integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"escape-string-regexp": {
|
"escape-string-regexp": {
|
||||||
@@ -573,14 +575,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fs-extra": {
|
"fs-extra": {
|
||||||
"version": "8.1.0",
|
"version": "11.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
|
||||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
"integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
"jsonfile": "^4.0.0",
|
"jsonfile": "^6.0.1",
|
||||||
"universalify": "^0.1.0"
|
"universalify": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fs.realpath": {
|
"fs.realpath": {
|
||||||
@@ -590,17 +592,17 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"gh-pages": {
|
"gh-pages": {
|
||||||
"version": "3.2.3",
|
"version": "6.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.1.1.tgz",
|
||||||
"integrity": "sha512-jA1PbapQ1jqzacECfjUaO9gV8uBgU6XNMV0oXLtfCX3haGLe5Atq8BxlrADhbD6/UdG9j6tZLWAkAybndOXTJg==",
|
"integrity": "sha512-upnohfjBwN5hBP9w2dPE7HO5JJTHzSGMV1JrLrHvNuqmjoYHg6TBrCcnEoorjG/e0ejbuvnwyKMdTyM40PEByw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"async": "^2.6.1",
|
"async": "^3.2.4",
|
||||||
"commander": "^2.18.0",
|
"commander": "^11.0.0",
|
||||||
"email-addresses": "^3.0.1",
|
"email-addresses": "^5.0.0",
|
||||||
"filenamify": "^4.3.0",
|
"filenamify": "^4.3.0",
|
||||||
"find-cache-dir": "^3.3.1",
|
"find-cache-dir": "^3.3.1",
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^11.1.1",
|
||||||
"globby": "^6.1.0"
|
"globby": "^6.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -632,9 +634,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"graceful-fs": {
|
"graceful-fs": {
|
||||||
"version": "4.2.8",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"inflight": {
|
"inflight": {
|
||||||
@@ -654,12 +656,13 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"jsonfile": {
|
"jsonfile": {
|
||||||
"version": "4.0.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||||
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"graceful-fs": "^4.1.6"
|
"graceful-fs": "^4.1.6",
|
||||||
|
"universalify": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"locate-path": {
|
"locate-path": {
|
||||||
@@ -671,12 +674,6 @@
|
|||||||
"p-locate": "^4.1.0"
|
"p-locate": "^4.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lodash": {
|
|
||||||
"version": "4.17.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"make-dir": {
|
"make-dir": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||||
@@ -801,9 +798,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"universalify": {
|
"universalify": {
|
||||||
"version": "0.1.2",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"wrappy": {
|
"wrappy": {
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"clean:l10n": "git push origin --delete l10n_master"
|
"clean:l10n": "git push origin --delete l10n_master"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"gh-pages": "3.2.3"
|
"gh-pages": "^6.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
<AndroidEnableMultiDex>True</AndroidEnableMultiDex>
|
<AndroidEnableMultiDex>True</AndroidEnableMultiDex>
|
||||||
<UseInterpreter>False</UseInterpreter>
|
<UseInterpreter>False</UseInterpreter>
|
||||||
<DebugSymbols>False</DebugSymbols>
|
<DebugSymbols>False</DebugSymbols>
|
||||||
<RunAOTCompilation>False</RunAOTCompilation>
|
<RunAOTCompilation>True</RunAOTCompilation>
|
||||||
<AndroidSupportedAbis>armeabi-v7a;x86;x86_64;arm64-v8a</AndroidSupportedAbis>
|
<AndroidSupportedAbis>armeabi-v7a;x86;x86_64;arm64-v8a</AndroidSupportedAbis>
|
||||||
<JavaMaximumHeapSize>1G</JavaMaximumHeapSize>
|
<JavaMaximumHeapSize>1G</JavaMaximumHeapSize>
|
||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
@@ -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,8 +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>
|
||||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">
|
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">
|
||||||
<BundleResource Include="Platforms\iOS\PrivacyInfo.xcprivacy" LogicalName="PrivacyInfo.xcprivacy" />
|
<BundleResource Include="Platforms\iOS\PrivacyInfo.xcprivacy" LogicalName="PrivacyInfo.xcprivacy" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
||||||
|
<MauiAsset Include="Resources\Raw\fido2_privileged_allow_list.json" LogicalName="fido2_privileged_allow_list.json" />
|
||||||
|
</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.4.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2024.10.111" 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" />
|
||||||
|
|||||||
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
|
||||||
{
|
{
|
||||||
@@ -203,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);
|
||||||
@@ -260,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);
|
||||||
@@ -337,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);
|
||||||
@@ -370,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,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);
|
||||||
@@ -474,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);
|
||||||
@@ -501,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);
|
||||||
}
|
}
|
||||||
@@ -514,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);
|
||||||
@@ -542,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;
|
||||||
@@ -559,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;
|
||||||
@@ -584,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.4.1</string>
|
<string>2024.10.111</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>CFBundleIconName</key>
|
<key>CFBundleIconName</key>
|
||||||
|
|||||||
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,7 +41,6 @@ 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,
|
||||||
@@ -100,5 +94,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,6 +34,7 @@ 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<bool> VerifyOrganizationHasUnassignedItemsAsync();
|
Task<string> CreateNewLoginForPasskeyAsync(Fido2ConfirmNewCredentialParams newPasskeyParams);
|
||||||
|
Task CopyTotpCodeIfNeededAsync(CipherView cipher);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +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
|
AndroidWindowCreated,
|
||||||
|
AutofillIOSExtensionViewDidAppear
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IConditionedAwaiterManager
|
public interface IConditionedAwaiterManager
|
||||||
@@ -14,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user