Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db88da0c33 | ||
|
|
62c4afeccd | ||
|
|
92f2953e3f | ||
|
|
de5e2c6c20 | ||
|
|
12cedf6d67 | ||
|
|
244acf0222 | ||
|
|
1f04c0d388 | ||
|
|
7e4369622a | ||
|
|
d8ba8f9395 | ||
|
|
c22c79364d | ||
|
|
b6e021069f | ||
|
|
eb0181cba6 | ||
|
|
92c462a4d9 | ||
|
|
a25174bb13 | ||
|
|
94209bffcd | ||
|
|
c8ad33b69b | ||
|
|
8a1d730d20 | ||
|
|
cd2ec5980a | ||
|
|
46b24b4a82 | ||
|
|
9ccadeef1b | ||
|
|
3e1cdd3798 | ||
|
|
17b5295684 | ||
|
|
139d416a50 | ||
|
|
83cdc517bb | ||
|
|
dccbf114ec | ||
|
|
a757d46202 | ||
|
|
b3f110e47f | ||
|
|
29d3a4dcf0 | ||
|
|
62330e0ed3 | ||
|
|
7b56da840e | ||
|
|
54ee509a26 | ||
|
|
aff5b74dc6 | ||
|
|
ed498678c3 | ||
|
|
e86f9dcfe4 | ||
|
|
4ab67c34bc | ||
|
|
afa0e47495 | ||
|
|
6fe154e268 | ||
|
|
5a1dbef16a | ||
|
|
d1e8d76d01 | ||
|
|
6d0f5d9dcc | ||
|
|
8d572bba88 | ||
|
|
2352b2f1ec | ||
|
|
d05db2168f | ||
|
|
087ee24e1d | ||
|
|
ddbe235e4f | ||
|
|
ef20df4776 | ||
|
|
af40f947b4 | ||
|
|
b3804be452 | ||
|
|
6c7e4269f7 | ||
|
|
bc5b795a97 | ||
|
|
a9a1fad71f | ||
|
|
3c9b08ca46 | ||
|
|
cc7d50d516 | ||
|
|
f0e14a8e08 | ||
|
|
a4c4284ad3 |
138
.editorconfig
Normal file
@ -0,0 +1,138 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Don't use tabs for indentation.
|
||||
[*]
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
tab_width = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
guidelines = 120
|
||||
|
||||
# Code files
|
||||
[*.{cs,csx,vb,vbx}]
|
||||
indent_size = 4
|
||||
|
||||
# Xml project files
|
||||
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
|
||||
indent_size = 2
|
||||
|
||||
# Xml config files
|
||||
[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
|
||||
indent_size = 2
|
||||
|
||||
# JSON files
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
|
||||
# JS files
|
||||
[*.{js,ts,scss,html}]
|
||||
indent_size = 2
|
||||
|
||||
[*.{ts}]
|
||||
quote_type = single
|
||||
|
||||
[*.{scss,yml,csproj}]
|
||||
indent_size = 2
|
||||
|
||||
[*.sln]
|
||||
indent_style = tab
|
||||
|
||||
# Dotnet code style settings:
|
||||
[*.{cs,vb}]
|
||||
# Sort using and Import directives with System.* appearing first
|
||||
dotnet_sort_system_directives_first = true
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
|
||||
# Use language keywords instead of framework type names for type references
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
|
||||
# Suggest more modern language features when available
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
|
||||
# Prefix private members with underscore
|
||||
dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
|
||||
dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
|
||||
dotnet_naming_rule.private_members_with_underscore.severity = suggestion
|
||||
|
||||
dotnet_naming_symbols.private_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
|
||||
|
||||
dotnet_naming_style.prefix_underscore.capitalization = camel_case
|
||||
dotnet_naming_style.prefix_underscore.required_prefix = _
|
||||
|
||||
# Async methods should have "Async" suffix
|
||||
dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods
|
||||
dotnet_naming_rule.async_methods_end_in_async.style = end_in_async
|
||||
dotnet_naming_rule.async_methods_end_in_async.severity = suggestion
|
||||
|
||||
dotnet_naming_symbols.any_async_methods.applicable_kinds = method
|
||||
dotnet_naming_symbols.any_async_methods.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.any_async_methods.required_modifiers = async
|
||||
|
||||
dotnet_naming_style.end_in_async.required_prefix =
|
||||
dotnet_naming_style.end_in_async.required_suffix = Async
|
||||
dotnet_naming_style.end_in_async.capitalization = pascal_case
|
||||
dotnet_naming_style.end_in_async.word_separator =
|
||||
|
||||
# Obsolete warnings, this should be removed or changed to warning once we address some of the obsolete items.
|
||||
dotnet_diagnostic.CS0618.severity = suggestion
|
||||
|
||||
# Obsolete warnings, this should be removed or changed to warning once we address some of the obsolete items.
|
||||
dotnet_diagnostic.CS0612.severity = suggestion
|
||||
|
||||
# Remove unnecessary using directives https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0005
|
||||
dotnet_diagnostic.IDE0005.severity = warning
|
||||
|
||||
# CSharp code style settings:
|
||||
[*.cs]
|
||||
# Prefer "var" everywhere
|
||||
csharp_style_var_for_built_in_types = true:suggestion
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
csharp_style_var_elsewhere = true:suggestion
|
||||
|
||||
# Prefer method-like constructs to have a expression-body
|
||||
csharp_style_expression_bodied_methods = true:none
|
||||
csharp_style_expression_bodied_constructors = true:none
|
||||
csharp_style_expression_bodied_operators = true:none
|
||||
|
||||
# Prefer property-like constructs to have an expression-body
|
||||
csharp_style_expression_bodied_properties = true:none
|
||||
csharp_style_expression_bodied_indexers = true:none
|
||||
csharp_style_expression_bodied_accessors = true:none
|
||||
|
||||
# Suggest more modern language features when available
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
|
||||
# Newline settings
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
|
||||
# Namespace settings
|
||||
csharp_style_namespace_declarations = file_scoped:warning
|
||||
|
||||
# Switch expression
|
||||
dotnet_diagnostic.CS8509.severity = error # missing switch case for named enum value
|
||||
dotnet_diagnostic.CS8524.severity = none # missing switch case for unnamed enum value
|
||||
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
11
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
# Please sort into logical groups with comment headers. Sort groups in order of specificity.
|
||||
# For example, default owners should always be the first group.
|
||||
# Sort lines alphabetically within these groups to avoid accidentally adding duplicates.
|
||||
#
|
||||
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
# Default file owners.
|
||||
* @bitwarden/team-tools
|
||||
|
||||
# DevOps for Actions and other workflow changes.
|
||||
.github/workflows @bitwarden/dept-devops
|
||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Feature Requests
|
||||
url: https://community.bitwarden.com/c/feature-requests/
|
||||
about: Request new features using the Community Forums. Please search existing feature requests before making a new one.
|
||||
- name: Bitwarden Community Forums
|
||||
url: https://community.bitwarden.com
|
||||
about: Please visit the community forums for general community discussion, support and the development roadmap.
|
||||
- name: Customer Support
|
||||
url: https://bitwarden.com/contact/
|
||||
about: Please contact our customer support for account issues and general customer support.
|
||||
- name: Security Issues
|
||||
url: https://hackerone.com/bitwarden
|
||||
about: We use HackerOne to manage security disclosures.
|
||||
51
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
## 🎟️ Tracking
|
||||
|
||||
<!-- Paste the link to the Jira or GitHub issue or otherwise describe / point to where this change is coming from. -->
|
||||
|
||||
## 🚧 Type of change
|
||||
|
||||
<!-- Choose those applicable and remove the others. -->
|
||||
|
||||
- 🐛 Bug fix
|
||||
- 🚀 New feature development
|
||||
- 🧹 Tech debt (refactoring, code cleanup, dependency upgrades, etc.)
|
||||
- 🤖 Build/deploy pipeline (DevOps)
|
||||
- 🎂 Other
|
||||
|
||||
## 📔 Objective
|
||||
|
||||
<!-- Describe what the purpose of this PR is, for example what bug you're fixing or new feature you're adding. -->
|
||||
|
||||
## 📋 Code changes
|
||||
|
||||
<!-- Explain the changes you've made to each file or major component. This should help the reviewer understand your changes. -->
|
||||
<!-- Also refer to any related changes or PRs in other repositories. -->
|
||||
|
||||
- **file.ext:** Description of what was changed and why.
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
<!-- Required for any UI changes; delete if not applicable. Use fixed width images for better display. -->
|
||||
|
||||
## ⏰ Reminders before review
|
||||
|
||||
- Contributor guidelines followed
|
||||
- All formatters and local linters executed and passed
|
||||
- Written new unit and / or integration tests where applicable
|
||||
- Used internationalization (i18n) for all UI strings
|
||||
- CI builds passed
|
||||
- Communicated to DevOps any deployment requirements
|
||||
- Updated any necessary documentation or informed the documentation team
|
||||
|
||||
## 🦮 Reviewer guidelines
|
||||
|
||||
<!-- Suggested interactions but feel free to use (or not) as you desire! -->
|
||||
|
||||
- 👍 (`:+1:`) or similar for great changes
|
||||
- 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info
|
||||
- ❓ (`:question:`) for questions
|
||||
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
|
||||
- 🎨 (`:art:`) for suggestions / improvements
|
||||
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention
|
||||
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt
|
||||
- ⛏ (`:pick:`) for minor or nitpick changes
|
||||
27
.github/renovate.json
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base",
|
||||
":combinePatchMinorReleases",
|
||||
":dependencyDashboard",
|
||||
":maintainLockFilesWeekly",
|
||||
":pinAllExceptPeerDependencies",
|
||||
":prConcurrentLimit10",
|
||||
":rebaseStalePrs",
|
||||
"schedule:weekends",
|
||||
":separateMajorReleases"
|
||||
],
|
||||
"enabledManagers": ["github-actions", "nuget"],
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "gh minor",
|
||||
"matchManagers": ["github-actions"],
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
},
|
||||
{
|
||||
"groupName": "nuget minor",
|
||||
"matchManagers": ["nuget"],
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
}
|
||||
]
|
||||
}
|
||||
26
.github/workflows/enforce-labels.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
name: Enforce PR labels
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
types: [labeled, unlabeled, opened, reopened, synchronize]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
enforce-label:
|
||||
if: ${{ contains(github.event.*.labels.*.name, 'hold') || contains(github.event.*.labels.*.name, 'needs-qa') }}
|
||||
name: Enforce label
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Check for label
|
||||
run: |
|
||||
echo "PRs with the hold or needs-qa labels cannot be merged"
|
||||
echo "### :x: PRs with the hold or needs-qa labels cannot be merged" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
17
.github/workflows/workflow-linter.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Workflow linter
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/**
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
call-workflow:
|
||||
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@main
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
5
.gitignore
vendored
@ -202,15 +202,10 @@ project.lock.json
|
||||
mail_dist/
|
||||
*.refactorlog
|
||||
*.scmp
|
||||
src/Core/Properties/launchSettings.json
|
||||
*.override.env
|
||||
**/*.DS_Store
|
||||
src/Admin/wwwroot/lib
|
||||
src/Admin/wwwroot/css
|
||||
.vscode/*
|
||||
**/.vscode/*
|
||||
bitwarden_license/src/Sso/wwwroot/lib
|
||||
bitwarden_license/src/Sso/wwwroot/css
|
||||
.github/test/build.secrets
|
||||
**/CoverageOutput/
|
||||
.idea/*
|
||||
|
||||
21
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": ".NET Core Launch (console)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "${workspaceFolder}/Importer/bin/Debug/net7.0-maccatalyst/Bitwarden Importer.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/Importer",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/bitwarden-importer.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/bitwarden-importer.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/bitwarden-importer.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
51
BUILD.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Build the macOS `.app` and `.pkg`
|
||||
|
||||
1. Run release build of the `Importer.csproj`.
|
||||
```
|
||||
dotnet build -f:net7.0-maccatalyst -c:Release
|
||||
```
|
||||
|
||||
Results (`.app` and `.pkg`) will be in the `./Importer/bin/Release/net7.0-maccatalyst/` directory.
|
||||
|
||||
# Notarize a macOS app
|
||||
|
||||
1. Use the included `notarizer.sh` script to notarize your application.
|
||||
```
|
||||
./notarizer.sh --notarize -a "./Importer/bin/Release/net7.0-maccatalyst/Bitwarden Importer.app" -b com.bitwarden.importer -u $APPLE_ID_USERNAME -p $APPLE_ID_PASSWORD -v LTZ2PFU5D6
|
||||
```
|
||||
|
||||
2. Check the status of the notarization process with Apple by running the check command with `notarizer.sh`. The RequestUUID is available from the response in running the previous command.
|
||||
```
|
||||
./notarizer.sh --check -u $APPLE_ID_USERNAME -p $APPLE_ID_PASSWORD -k <REQUEST_UUID>
|
||||
```
|
||||
|
||||
3. Once notarization is successful, staple the notarized application.
|
||||
```
|
||||
./notarizer.sh --staple --file "./Importer/bin/Release/net7.0-maccatalyst/Bitwarden Importer.app"
|
||||
```
|
||||
|
||||
# Build the macOS `.zip` artifact
|
||||
|
||||
1. Follow steps for building the macOS `.app`.
|
||||
|
||||
2. Notarize the `Bitwarden Importer.app` by following the steps for notarizing a macOS app.
|
||||
|
||||
3. Zip up the `Bitwarden Importer.app` file for publishing.
|
||||
|
||||
# Build the macOS `.pkg` artifact
|
||||
|
||||
1. Follow steps for building the macOS `.pkg`.
|
||||
|
||||
2. Notarize the `.pkg` by following the steps for notarizing a macOS app.
|
||||
|
||||
# Build the Windows `.msix` artifact
|
||||
|
||||
1. Run release build of the `Importer.csproj`.
|
||||
```
|
||||
dotnet publish -f net7.0-windows10.0.19041.0 -c Release /p:RuntimeIdentifierOverride=win10-x64
|
||||
```
|
||||
|
||||
2. Sign the created `.msix` with `azuresigntool`
|
||||
```
|
||||
azuresigntool.exe sign -v -kvu <URL> -kvi <ID> -kvt <TENANT> -kvs <SECRET> -kvc code-signing-certificate-3 -tr http://timestamp.digicert.com .\Importer_1.0.0.0_x64.msix
|
||||
```
|
||||
@ -1,22 +0,0 @@
|
||||
namespace Bit.Importer;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public App()
|
||||
{
|
||||
// Uncomment below to force light theme
|
||||
// Current.UserAppTheme = AppTheme.Light;
|
||||
InitializeComponent();
|
||||
|
||||
MainPage = new AppShell();
|
||||
}
|
||||
|
||||
protected override Window CreateWindow(IActivationState activationState)
|
||||
{
|
||||
var window = base.CreateWindow(activationState);
|
||||
window.Width = 650;
|
||||
window.Height = 1100;
|
||||
window.Title = "Bitwarden Importer";
|
||||
return window;
|
||||
}
|
||||
}
|
||||
@ -1,151 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.Importer.MainPage">
|
||||
|
||||
<ScrollView>
|
||||
<VerticalStackLayout
|
||||
Spacing="50"
|
||||
Padding="20"
|
||||
VerticalOptions="Start">
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="10"
|
||||
Padding="0">
|
||||
<Label
|
||||
Text="The Bitwarden Importer Tool helps you easily move all of your data from an existing password management service into your Bitwarden account. Simply input your credentials for Bitwarden and your old password management service into the form below and click the 'Import' button." />
|
||||
<Label
|
||||
Text="Click here to learn more"
|
||||
TextColor="{StaticResource Primary}"
|
||||
FontAttributes="Bold"
|
||||
TextDecorations="Underline"
|
||||
x:Name="LearnMore" />
|
||||
</VerticalStackLayout>
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="20"
|
||||
Padding="0">
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<Label
|
||||
Text="Bitwarden Server Url" />
|
||||
<Entry
|
||||
x:Name="BitwardenServerUrl" />
|
||||
</VerticalStackLayout>
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<HorizontalStackLayout
|
||||
Spacing="10">
|
||||
<Label
|
||||
Text="Bitwarden API Key client_id" />
|
||||
<Label
|
||||
Text="Click here to get your API key"
|
||||
TextColor="{StaticResource Primary}"
|
||||
FontAttributes="Bold"
|
||||
TextDecorations="Underline"
|
||||
x:Name="ApiKeyLink1" />
|
||||
</HorizontalStackLayout>
|
||||
<Entry
|
||||
x:Name="BitwardenApiKeyClientId" />
|
||||
</VerticalStackLayout>
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<HorizontalStackLayout
|
||||
Spacing="10">
|
||||
<Label
|
||||
Text="Bitwarden API Key secret" />
|
||||
<Label
|
||||
Text="Click here to get your API key"
|
||||
TextColor="{StaticResource Primary}"
|
||||
FontAttributes="Bold"
|
||||
TextDecorations="Underline"
|
||||
x:Name="ApiKeyLink2"/>
|
||||
</HorizontalStackLayout>
|
||||
<Entry
|
||||
x:Name="BitwardenApiKeySecret"
|
||||
IsPassword="True" />
|
||||
</VerticalStackLayout>
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0"
|
||||
x:Name="BitwardenPasswordLayout">
|
||||
<Label
|
||||
Text="Bitwarden Master Password" />
|
||||
<Entry
|
||||
x:Name="BitwardenPassword"
|
||||
IsPassword="True" />
|
||||
</VerticalStackLayout>
|
||||
<HorizontalStackLayout
|
||||
x:Name="BitwardenKeyConnectorLayout">
|
||||
<CheckBox
|
||||
x:Name="BitwardenKeyConnector"
|
||||
CheckedChanged="BitwardenKeyConnector_CheckedChanged"
|
||||
VerticalOptions="Center" />
|
||||
<Label
|
||||
x:Name="BitwardenKeyConnectorLabel"
|
||||
Text="My organization uses a SSO configuration that does not require a master password."
|
||||
VerticalOptions="Center" />
|
||||
</HorizontalStackLayout>
|
||||
</VerticalStackLayout>
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="20"
|
||||
Padding="0">
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<Label
|
||||
Text="Import From Service" />
|
||||
<Picker
|
||||
x:Name="Service" />
|
||||
</VerticalStackLayout>
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<Label
|
||||
Text="LastPass Email" />
|
||||
<Entry
|
||||
x:Name="LastPassEmail" />
|
||||
</VerticalStackLayout>
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<Label
|
||||
Text="LastPass Master Password" />
|
||||
<Entry
|
||||
x:Name="LastPassPassword"
|
||||
IsPassword="True" />
|
||||
</VerticalStackLayout>
|
||||
</VerticalStackLayout>
|
||||
|
||||
<HorizontalStackLayout
|
||||
Spacing="10"
|
||||
Padding="0">
|
||||
<Button
|
||||
x:Name="SubmitButton"
|
||||
Text="Import"
|
||||
Clicked="OnButtonClicked"
|
||||
HorizontalOptions="Center" />
|
||||
<ActivityIndicator
|
||||
x:Name="Loading"
|
||||
IsRunning="false"
|
||||
VerticalOptions="Center" />
|
||||
<Label
|
||||
x:Name="PleaseWait"
|
||||
Text="Please wait..."
|
||||
IsVisible="false"
|
||||
VerticalOptions="Center" />
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<Label
|
||||
x:Name="CachePath"
|
||||
IsVisible="false" />
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</ContentPage>
|
||||
@ -1,421 +0,0 @@
|
||||
using CsvHelper;
|
||||
using PasswordManagerAccess.LastPass;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Bit.Importer;
|
||||
|
||||
public partial class MainPage : ContentPage
|
||||
{
|
||||
private readonly string _cacheDir;
|
||||
private readonly List<string> _services = new() { "LastPass" };
|
||||
private string _bitwardenCloudUrl = "https://bitwarden.com";
|
||||
|
||||
public MainPage()
|
||||
{
|
||||
_cacheDir = Path.Combine(FileSystem.CacheDirectory, "com.bitwarden.importer");
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
BitwardenServerUrl.Text = _bitwardenCloudUrl;
|
||||
Service.ItemsSource = _services;
|
||||
Service.SelectedIndex = 0;
|
||||
|
||||
var learnMoreTap = new TapGestureRecognizer();
|
||||
learnMoreTap.Tapped += async (s, e) =>
|
||||
{
|
||||
await Browser.Default.OpenAsync(new Uri("https://bitwarden.com/help"),
|
||||
BrowserLaunchMode.SystemPreferred);
|
||||
};
|
||||
LearnMore.GestureRecognizers.Add(learnMoreTap);
|
||||
|
||||
var apiKeyTap = new TapGestureRecognizer();
|
||||
apiKeyTap.Tapped += async (s, e) =>
|
||||
{
|
||||
await Browser.Default.OpenAsync(new Uri("https://vault.bitwarden.com/#/settings/security/security-keys"),
|
||||
BrowserLaunchMode.SystemPreferred);
|
||||
};
|
||||
ApiKeyLink1.GestureRecognizers.Add(apiKeyTap);
|
||||
ApiKeyLink2.GestureRecognizers.Add(apiKeyTap);
|
||||
|
||||
CachePath.Text = _cacheDir;
|
||||
ParseCommandlineDefaults();
|
||||
}
|
||||
|
||||
private void BitwardenKeyConnector_CheckedChanged(object sender, CheckedChangedEventArgs e)
|
||||
{
|
||||
// We don't need master passwqord if using key connector
|
||||
BitwardenPasswordLayout.IsVisible = !BitwardenKeyConnector.IsChecked;
|
||||
}
|
||||
|
||||
private async void OnButtonClicked(object sender, EventArgs e)
|
||||
{
|
||||
// Validate
|
||||
if (!await ValidateInputsAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Start loading state
|
||||
Loading.IsRunning = true;
|
||||
SubmitButton.IsEnabled = false;
|
||||
PleaseWait.IsVisible = true;
|
||||
|
||||
// Run the import task
|
||||
await Task.Run(ImportAsync);
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateInputsAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BitwardenApiKeyClientId?.Text) ||
|
||||
string.IsNullOrWhiteSpace(BitwardenApiKeySecret?.Text))
|
||||
{
|
||||
await DisplayAlert("Error", "Bitwarden API Key information is required.", "OK");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(BitwardenPassword?.Text) &&
|
||||
!BitwardenKeyConnector.IsChecked)
|
||||
{
|
||||
await DisplayAlert("Error", "Bitwarden master password is required.", "OK");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_services[Service.SelectedIndex] == "LastPass")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(LastPassEmail?.Text) ||
|
||||
string.IsNullOrWhiteSpace(LastPassPassword?.Text))
|
||||
{
|
||||
await DisplayAlert("Error", "LastPass Email and Master Password are required.", "OK");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task ImportAsync()
|
||||
{
|
||||
await SetupAsync();
|
||||
await CleanupAsync();
|
||||
|
||||
if (_services[Service.SelectedIndex] == "LastPass")
|
||||
{
|
||||
var (lastpassSuccess, lastpassCsvPath) = await CreateLastpassCsvAsync();
|
||||
if (!lastpassSuccess)
|
||||
{
|
||||
StopLoadingAndAlert(true,
|
||||
"Unable to log into your LastPass account. Are your credentials correct?");
|
||||
}
|
||||
|
||||
var cliSetupSuccess = false;
|
||||
try
|
||||
{
|
||||
if (lastpassSuccess)
|
||||
{
|
||||
await SetupCliAsync();
|
||||
cliSetupSuccess = true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
StopLoadingAndAlert(true, "Unable to set up Bitwarden CLI.");
|
||||
}
|
||||
|
||||
if (cliSetupSuccess && lastpassSuccess)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(BitwardenServerUrl?.Text) && BitwardenServerUrl.Text != "https://bitwarden.com")
|
||||
{
|
||||
var configServerSuccess = ConfigServerCli();
|
||||
if (!configServerSuccess)
|
||||
{
|
||||
StopLoadingAndAlert(true, "Unable to configure Bitwarden server.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var (loginSuccess, sessionKey) = LogInCli();
|
||||
if (!loginSuccess)
|
||||
{
|
||||
StopLoadingAndAlert(true,
|
||||
"Unable to log into your Bitwarden account. Is your API key information correct?");
|
||||
}
|
||||
|
||||
if (loginSuccess && string.IsNullOrWhiteSpace(sessionKey))
|
||||
{
|
||||
var (unlockSuccess, unlockSessionKey) = UnlockCli();
|
||||
sessionKey = unlockSessionKey;
|
||||
if (!unlockSuccess)
|
||||
{
|
||||
StopLoadingAndAlert(true,
|
||||
"Unable to unlock your Bitwarden vault. Is your master password correct?");
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(lastpassCsvPath) && !string.IsNullOrWhiteSpace(sessionKey))
|
||||
{
|
||||
var importSuccess = ImportCli("lastpasscsv", lastpassCsvPath, sessionKey);
|
||||
if (importSuccess)
|
||||
{
|
||||
StopLoadingAndAlert(false, "Your import was successful!");
|
||||
ClearInputs();
|
||||
}
|
||||
else
|
||||
{
|
||||
StopLoadingAndAlert(true, "Something went wrong with the import.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await CleanupAsync();
|
||||
}
|
||||
|
||||
private void StopLoadingAndAlert(bool error, string message)
|
||||
{
|
||||
Dispatcher.Dispatch(async () =>
|
||||
{
|
||||
// Stop the loading state
|
||||
Loading.IsRunning = false;
|
||||
SubmitButton.IsEnabled = true;
|
||||
PleaseWait.IsVisible = false;
|
||||
|
||||
// Show alert
|
||||
if (error)
|
||||
{
|
||||
await DisplayAlert("Error", message, "OK");
|
||||
}
|
||||
else
|
||||
{
|
||||
await DisplayAlert("Success", message, "OK");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<(bool, string)> CreateLastpassCsvAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Log in and get LastPass data
|
||||
var ui = new Services.LastPass.Ui(this);
|
||||
var clientInfo = new ClientInfo(
|
||||
PasswordManagerAccess.LastPass.Platform.Desktop,
|
||||
Guid.NewGuid().ToString().ToLower(),
|
||||
"Importer");
|
||||
var vault = Vault.Open(LastPassEmail?.Text, LastPassPassword?.Text, clientInfo, ui);
|
||||
|
||||
// Massage it to expected CSV format
|
||||
var exportAccounts = vault.Accounts.Select(a => new Services.LastPass.ExportedAccount(a));
|
||||
|
||||
// Create CSV string
|
||||
using var writer = new StringWriter();
|
||||
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
|
||||
csv.WriteRecords(exportAccounts);
|
||||
csv.Flush();
|
||||
var csvOutput = writer.ToString();
|
||||
|
||||
// Write CSV to temp disk
|
||||
var lastpassCsvPath = Path.Combine(_cacheDir, "lastpass-export.csv");
|
||||
await File.WriteAllTextAsync(lastpassCsvPath, csvOutput);
|
||||
return (true, lastpassCsvPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (false, null);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ConfigServerCli()
|
||||
{
|
||||
var (exitCode, stdOut) = ExecCli($"config server {BitwardenServerUrl?.Text}");
|
||||
return exitCode == 0;
|
||||
}
|
||||
|
||||
private (bool, string) LogInCli()
|
||||
{
|
||||
var (exitCode, sessionKey) = ExecCli("login --apikey --raw", (process) =>
|
||||
{
|
||||
process.StartInfo.EnvironmentVariables["BW_CLIENTID"] = BitwardenApiKeyClientId?.Text;
|
||||
process.StartInfo.EnvironmentVariables["BW_CLIENTSECRET"] = BitwardenApiKeySecret?.Text;
|
||||
// Avoid BW_NOINTERACTION bug that is issuing invalid session key on api key login
|
||||
process.StartInfo.EnvironmentVariables["BW_NOINTERACTION"] = "false";
|
||||
});
|
||||
return (exitCode == 0, sessionKey);
|
||||
}
|
||||
|
||||
private (bool, string) UnlockCli()
|
||||
{
|
||||
var (exitCode, sessionKey) = ExecCli($"unlock {BitwardenPassword?.Text} --raw");
|
||||
return (exitCode == 0, sessionKey);
|
||||
}
|
||||
|
||||
private bool ImportCli(string importService, string importFilePath, string sessionKey)
|
||||
{
|
||||
var (importExitCode, importStdOut) = ExecCli($"import {importService} {importFilePath}", (process) =>
|
||||
{
|
||||
process.StartInfo.EnvironmentVariables["BW_SESSION"] = sessionKey;
|
||||
});
|
||||
return importExitCode == 0 && (importStdOut?.Contains("Imported") ?? false);
|
||||
}
|
||||
|
||||
private async Task SetupCliAsync()
|
||||
{
|
||||
// Copy packaged CLI app to disk so that we can invoke it.
|
||||
var isWindows = DeviceInfo.Platform == DevicePlatform.WinUI;
|
||||
var cliFilename = isWindows ? "bw-windows.exe" : "bw-mac";
|
||||
var cliPath = ResolveCliPath();
|
||||
using var stream = await FileSystem.OpenAppPackageFileAsync($"bw-cli/{cliFilename}");
|
||||
using var fileStream = File.Create(cliPath);
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
stream.CopyTo(fileStream);
|
||||
stream.Close();
|
||||
fileStream.Close();
|
||||
if (!isWindows)
|
||||
{
|
||||
ExecBash($"chmod +x {cliPath}");
|
||||
}
|
||||
}
|
||||
|
||||
private (int, string) ExecCli(string args, Action<Process> processAction = null)
|
||||
{
|
||||
// Set up the process
|
||||
using var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = ResolveCliPath(),
|
||||
Arguments = args,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
CreateNoWindow = true
|
||||
},
|
||||
};
|
||||
|
||||
// Load standard env vars for this use case
|
||||
process.StartInfo.EnvironmentVariables["BITWARDENCLI_APPDATA_DIR"] = _cacheDir;
|
||||
process.StartInfo.EnvironmentVariables["BW_NOINTERACTION"] = "true";
|
||||
processAction?.Invoke(process);
|
||||
|
||||
process.Start();
|
||||
var stdOut = "";
|
||||
while (!process.StandardOutput.EndOfStream)
|
||||
{
|
||||
stdOut += process.StandardOutput.ReadLine();
|
||||
}
|
||||
process.StandardOutput.Close();
|
||||
process.WaitForExit();
|
||||
return (process.ExitCode, stdOut.Trim());
|
||||
}
|
||||
|
||||
private string ResolveCliPath()
|
||||
{
|
||||
var bwCliFilename = DeviceInfo.Platform == DevicePlatform.WinUI ? "bw.exe" : "bw";
|
||||
return Path.Combine(_cacheDir, bwCliFilename);
|
||||
}
|
||||
|
||||
private Task CleanupAsync()
|
||||
{
|
||||
File.Delete(Path.Combine(_cacheDir, "data.json"));
|
||||
File.Delete(Path.Combine(_cacheDir, "lastpass-export.csv"));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
private Task SetupAsync()
|
||||
{
|
||||
if (!Directory.Exists(_cacheDir))
|
||||
{
|
||||
Directory.CreateDirectory(_cacheDir);
|
||||
}
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public static void ExecBash(string cmd)
|
||||
{
|
||||
var escapedArgs = cmd.Replace("\"", "\\\"");
|
||||
using var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
FileName = "/bin/bash",
|
||||
Arguments = $"-c \"{escapedArgs}\""
|
||||
}
|
||||
};
|
||||
process.Start();
|
||||
process.WaitForExit();
|
||||
}
|
||||
|
||||
private void ClearInputs()
|
||||
{
|
||||
BitwardenServerUrl.Text = string.Empty;
|
||||
BitwardenApiKeyClientId.Text = string.Empty;
|
||||
BitwardenApiKeySecret.Text = string.Empty;
|
||||
BitwardenKeyConnector.IsChecked = false;
|
||||
BitwardenPassword.Text = string.Empty;
|
||||
LastPassEmail.Text = string.Empty;
|
||||
LastPassPassword.Text = string.Empty;
|
||||
}
|
||||
|
||||
private void ParseCommandlineDefaults()
|
||||
{
|
||||
var args = Environment.GetCommandLineArgs();
|
||||
foreach (var arg in args)
|
||||
{
|
||||
if (!arg.Contains("="))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var argParts = arg.Split('=');
|
||||
if (argParts.Length < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "bitwardenServerUrl")
|
||||
{
|
||||
BitwardenServerUrl.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "bitwardenApiKeyClientId")
|
||||
{
|
||||
BitwardenApiKeyClientId.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "bitwardenApiKeySecret")
|
||||
{
|
||||
BitwardenApiKeySecret.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "bitwardenMasterPassword")
|
||||
{
|
||||
BitwardenPassword.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "bitwardenKeyConnector")
|
||||
{
|
||||
BitwardenKeyConnector.IsChecked = argParts[1] == "1";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "lastpassEmail")
|
||||
{
|
||||
LastPassEmail.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "lastpassMasterPassword")
|
||||
{
|
||||
LastPassPassword.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
using PasswordManagerAccess.Common;
|
||||
using PasswordManagerAccess.LastPass.Ui;
|
||||
|
||||
namespace Bit.Importer.Services.LastPass;
|
||||
|
||||
public class Ui : IUi
|
||||
{
|
||||
private readonly MainPage _page;
|
||||
|
||||
public Ui(MainPage page)
|
||||
{
|
||||
_page = page;
|
||||
}
|
||||
|
||||
public OtpResult ProvideGoogleAuthPasscode()
|
||||
{
|
||||
return new OtpResult(PromptCode("Enter your authenticator two-step login code."), false);
|
||||
}
|
||||
|
||||
public OtpResult ProvideMicrosoftAuthPasscode()
|
||||
{
|
||||
return new OtpResult(PromptCode("Enter your authenticator two-step login code."), false);
|
||||
}
|
||||
|
||||
public OtpResult ProvideYubikeyPasscode()
|
||||
{
|
||||
return new OtpResult(PromptCode("Enter your Yubikey code."), false);
|
||||
}
|
||||
|
||||
public OobResult ApproveLastPassAuth()
|
||||
{
|
||||
return OobResult.ContinueWithPasscode(PromptCode("Enter passcode from LastPass authenticator."), false);
|
||||
}
|
||||
|
||||
public OobResult ApproveDuo()
|
||||
{
|
||||
return OobResult.ContinueWithPasscode(PromptCode("Enter passcode from Duo."), false);
|
||||
}
|
||||
|
||||
public DuoChoice ChooseDuoFactor(DuoDevice[] devices)
|
||||
{
|
||||
var task = _page.Dispatcher.DispatchAsync(() =>
|
||||
_page.DisplayActionSheet("Choose a Duo device", "Cancel", null, devices.Select(d => d.Name).ToArray()));
|
||||
var actionSelection = task.GetAwaiter().GetResult();
|
||||
var device = devices.FirstOrDefault(d => d.Name == actionSelection);
|
||||
return new DuoChoice(device, DuoFactor.Passcode, false);
|
||||
}
|
||||
|
||||
public string ProvideDuoPasscode(DuoDevice device)
|
||||
{
|
||||
return PromptCode("Enter your Duo passcode.");
|
||||
}
|
||||
|
||||
public void UpdateDuoStatus(DuoStatus status, string text)
|
||||
{
|
||||
// Not sure what this is for.
|
||||
}
|
||||
|
||||
private string PromptCode(string message)
|
||||
{
|
||||
var task = _page.Dispatcher.DispatchAsync(() =>
|
||||
_page.DisplayPromptAsync("LastPass Two-step Login", message, "Submit"));
|
||||
return task.GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
31
README.md
@ -1,2 +1,29 @@
|
||||
# importer
|
||||
Bitwarden standalone importer tool.
|
||||
> **Archived**
|
||||
>
|
||||
> This repository is archived. Please go to https://bitwarden.com/help/import-from-lastpass/ for information on directly importing from LastPass.
|
||||
|
||||
# Bitwarden Importer
|
||||
|
||||
The Bitwarden Importer utility can be used to migrate individual vaults from another password management service, such as LastPass, without having to deal with the typical export process requiring CSV files. Just enter your credentials from Bitwarden and the old password management service and you're done!
|
||||
|
||||

|
||||
|
||||
## Command line args
|
||||
|
||||
You can use command line arguments to pre-populate any of the fields with default values:
|
||||
|
||||
- `bitwardenServerUrl=https://bitwarden.company.com`
|
||||
- `bitwardenEmail=john.doe@company.com`
|
||||
- `bitwardenApiKey=1` (1 = checked)
|
||||
- `bitwardenApiKeyClientId=user.guid`
|
||||
- `bitwardenApiKeySecret=myApiKeySecret`
|
||||
- `bitwardenMasterPassword=my-bitwarden-master-password`
|
||||
- `bitwardenKeyConnector=1` (1 = checked)
|
||||
- `lastpassEmail=john.doe@company.com`
|
||||
- `lastpassMasterPassword=my-lastpass-master-password`
|
||||
- `lastpassSkipShared=1` (1 = checked)
|
||||
- `disableLastpassSkipShared=1` (1 = disabled)
|
||||
|
||||
## Special thanks
|
||||
|
||||
A special thank you to the [Password Manager Access](https://github.com/detunized/password-manager-access) library that powers this application.
|
||||
|
||||
@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31611.283
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Importer", "Importer\Importer.csproj", "{46E1189D-8B50-4C55-A443-F4B1BBFF7504}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Importer", "src\Importer\Importer.csproj", "{46E1189D-8B50-4C55-A443-F4B1BBFF7504}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
||||
6
global.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "7.0.401",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
286
notarizer.sh
Executable file
@ -0,0 +1,286 @@
|
||||
#!/bin/bash
|
||||
|
||||
usage()
|
||||
{
|
||||
# Display Help
|
||||
|
||||
echo "*********************************************************************************************************************************************************"
|
||||
echo " MacOS Application Notarization Script"
|
||||
echo "*********************************************************************************************************************************************************"
|
||||
echo
|
||||
echo " Requirements"
|
||||
echo " - XCode Installed"
|
||||
echo " - Apple Id Account app-specific Password (https://support.apple.com/en-us/HT204397)"
|
||||
echo " - Apple Developer ID Application Certificate created and installed in keychain (https://developer.apple.com/support/developer-id/)"
|
||||
echo
|
||||
echo " Instructions"
|
||||
echo " 1. Run notarize option to code sign application and create notarization request"
|
||||
echo " 2. Run check option with the request UUID of the previous step to check the notarization status"
|
||||
echo " 3. Run staple option only if the notarization status was successful and package was approved"
|
||||
echo " 4. You are now ready to distribute, if you want to create an installer you can use this option https://github.com/sindresorhus/create-dmg."
|
||||
echo " Note that if you distribute your app in a .dmg, follow these steps:"
|
||||
echo
|
||||
echo " - Add your notarized and stapled app to the DMG."
|
||||
echo " - Notarize your .dmg file."
|
||||
echo " Example: sh $0 --notarize -a MyApp.dmg -b com.company.myapp -u myappleaccount@gmail.com -p aaaa-aaaa-aaaa-aaa -v FFFFFFFF)"
|
||||
echo " - Staple the notarization to the .dmg file: xcrun stapler staple MyApp.dmg."
|
||||
echo " Example: sh $0 --staple --file MyApp.dmg"
|
||||
echo "________________________________________________________________________________________________________________________________________________________"
|
||||
echo
|
||||
echo " Usage"
|
||||
echo " $0 [-n|s|c] [ -a APP_NAME ] [ -i SIGNING_IDENTITY ] [ -e ENTITLEMENTS ] [ -b BUNDLE_ID ] [ -u USERNAME ] [ -p PASSWORD ] [ -v PROVIDER ] [ -k UUID ]"
|
||||
echo
|
||||
echo "________________________________________________________________________________________________________________________________________________________"
|
||||
echo
|
||||
echo " Options:"
|
||||
echo
|
||||
notarizeHelp
|
||||
checkHelp
|
||||
stapleHelp
|
||||
|
||||
return
|
||||
}
|
||||
notarizeHelp()
|
||||
{
|
||||
echo " ======================================================================="
|
||||
echo " -n | --notarize Notarize file"
|
||||
echo " ======================================================================="
|
||||
echo " Syntax:"
|
||||
echo " [ -n | --notarize ] [ -a | --file APP_NAME ] [ -i SIGNING_IDENTITY ] [ -e ENTITLEMENTS ] [ -b BUNDLE_ID ] [ -u USERNAME ] [ -p PASSWORD ] [ -v PROVIDER ]"
|
||||
echo " Parameters:"
|
||||
echo " [ -a | --file ] - File name"
|
||||
echo " [ -i ] - Apple Signing identity"
|
||||
echo " [ -e ] - Application entitlements file"
|
||||
echo " [ -b ] - Application Bundle identifier"
|
||||
echo " [ -u ] - Apple Developer ID Username"
|
||||
echo " [ -p ] - Application Specific password"
|
||||
echo " [ -v ] - Access Provider"
|
||||
echo " Example:"
|
||||
echo " .app sh $0 --notarize -a MyApp.app -b com.company.myapp -u myappleaccount@gmail.com -p aaaa-aaaa-aaaa-aaa -v FFFFFFFF -e App.entitlements -i \"Developer ID Application: COMPANY\""
|
||||
echo " .zip sh $0 --notarize -a MyApp.app.zip -b com.company.myapp -u myappleaccount@gmail.com -p aaaa-aaaa-aaaa-aaa -v FFFFFFFF"
|
||||
echo " .dmg sh $0 --notarize -a MyApp.dmg -b com.company.myapp -u myappleaccount@gmail.com -p aaaa-aaaa-aaaa-aaa -v FFFFFFFF"
|
||||
echo
|
||||
}
|
||||
checkHelp()
|
||||
{
|
||||
echo " ======================================================================="
|
||||
echo " -c | --check Check notarization status"
|
||||
echo " ======================================================================="
|
||||
echo " Syntax:"
|
||||
echo " [ -c | --check ] [ -u USERNAME ] [ -p PASSWORD ] [ -k UUID ]"
|
||||
echo " Parameters:"
|
||||
echo " [ -u ] - Apple Developer ID Username"
|
||||
echo " [ -p ] - Application Specific password"
|
||||
echo " [ -k ] - Notarization Request UUID"
|
||||
echo " Example:"
|
||||
echo " sh $0 --check -u myappleaccount@gmail.com -p aaaa-aaaa-aaaa-aaa -k ffff-ffffff-ffffff-ffffffffff"
|
||||
echo
|
||||
}
|
||||
stapleHelp()
|
||||
{
|
||||
echo " ======================================================================="
|
||||
echo " -s | --staple Staple file"
|
||||
echo " ======================================================================="
|
||||
echo " Syntax:"
|
||||
echo " [ -s | --staple ] [ -a | --file APP_NAME ]"
|
||||
echo " Parameters:"
|
||||
echo " [ -a | --file ] - File name"
|
||||
echo " Example:"
|
||||
echo " sh $0 --staple --file MyApp.app"
|
||||
echo
|
||||
}
|
||||
|
||||
#Help Dictionary
|
||||
helpFunction()
|
||||
{
|
||||
|
||||
echo ""
|
||||
usage
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Transform long options to short ones
|
||||
for arg in "$@"; do
|
||||
shift
|
||||
case "$arg" in
|
||||
"--notarize") set -- "$@" "-n" ;;
|
||||
"--staple") set -- "$@" "-s" ;;
|
||||
"--check") set -- "$@" "-c" ;;
|
||||
"--file") set -- "$@" "-a" ;;
|
||||
*) set -- "$@" "$arg"
|
||||
esac
|
||||
done
|
||||
|
||||
|
||||
while getopts "nsca:i:e:b:v:u:k:p:" option
|
||||
do
|
||||
case "${option}"
|
||||
in
|
||||
n) ACTION=NOTARIZE;;
|
||||
s) ACTION=STAPLE;;
|
||||
c) ACTION=CHECK;;
|
||||
a) APP_NAME=${OPTARG};;
|
||||
i) SIGNING_IDENTITY=${OPTARG};;
|
||||
e) ENTITLEMENTS=${OPTARG};;
|
||||
b) BUNDLE_ID=${OPTARG};;
|
||||
p) PASSWORD=${OPTARG};;
|
||||
v) PROVIDER=${OPTARG};;
|
||||
u) USERNAME=${OPTARG};;
|
||||
k) UUID=${OPTARG};;
|
||||
?) helpFunction ;;
|
||||
esac
|
||||
done
|
||||
|
||||
|
||||
do_check()
|
||||
{
|
||||
echo "$UUID"
|
||||
if [ -z "${UUID}" ]; then
|
||||
echo "[Error] Didn't specify notarization request UUID";
|
||||
fi
|
||||
|
||||
if [ -z "${USERNAME}" ]; then
|
||||
echo "[Error] Apple ID username is required";
|
||||
fi
|
||||
|
||||
if [ -z "${PASSWORD}" ]; then
|
||||
echo "[Error] App Specific password is required";
|
||||
fi
|
||||
|
||||
if [ -z "${UUID}" ] || [ -z "${USERNAME}" ] || [ -z "${PASSWORD}" ]; then
|
||||
echo
|
||||
checkHelp
|
||||
exit 1
|
||||
fi
|
||||
echo "[INFO] Checking Notarization status for $UUID"
|
||||
xcrun altool --notarization-info "$UUID" -u "$USERNAME" -p "$PASSWORD" --output-format xml
|
||||
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
|
||||
sign()
|
||||
{
|
||||
if [ -z "${APP_NAME}" ]; then
|
||||
echo "[Error] Didn't specify a filename";
|
||||
fi
|
||||
if [ -z "${BUNDLE_ID}" ]; then
|
||||
echo "[Error] Didn't specify bundle identifier";
|
||||
fi
|
||||
if [ -z "${PROVIDER}" ]; then
|
||||
echo "[Error] Didn't specify access provider";
|
||||
fi
|
||||
if [ -z "${USERNAME}" ]; then
|
||||
echo "[Error] Apple ID username is required";
|
||||
fi
|
||||
if [ -z "${PASSWORD}" ]; then
|
||||
echo "[Error] App Specific password is required";
|
||||
fi
|
||||
|
||||
if [ -z "${APP_NAME}" ] || [ -z "${USERNAME}" ] || [ -z "${PASSWORD}" ] || [ -z "${PROVIDER}" ]; then
|
||||
echo
|
||||
notarizeHelp
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#echo "[INFO] Signing app contents"
|
||||
#find "$APP_NAME/Contents"|while read fname; do
|
||||
# if [[ -f $fname ]]; then
|
||||
# echo "[INFO] Signing $fname"
|
||||
# codesign --force --timestamp --options=runtime --entitlements "$ENTITLEMENTS" --sign "$SIGNING_IDENTITY" $fname
|
||||
# fi
|
||||
#done
|
||||
|
||||
#echo "[INFO] Signing app file"
|
||||
|
||||
|
||||
#codesign --force --timestamp --options=runtime --entitlements "$ENTITLEMENTS" --sign "$SIGNING_IDENTITY" "$APP_NAME"
|
||||
|
||||
echo "[INFO] Verifying Code Sign"
|
||||
|
||||
codesign --verify --verbose "$APP_NAME"
|
||||
|
||||
echo "[INFO] Zipping $APP_NAME to ${APP_NAME}.zip"
|
||||
|
||||
ditto -c -k --rsrc --keepParent "$APP_NAME" "${APP_NAME}.zip"
|
||||
|
||||
#echo "[INFO] Uploading $APP_NAME for notarization"
|
||||
|
||||
#xcrun altool --notarize-app -t osx -f "${APP_NAME}.zip" --primary-bundle-id "$BUNDLE_ID" -u "$USERNAME" -p "$PASSWORD" --asc-provider "$PROVIDER" --output-format xml
|
||||
|
||||
notarizationUpload "${APP_NAME}.zip"
|
||||
}
|
||||
|
||||
notarize()
|
||||
{
|
||||
|
||||
if [ -z "${APP_NAME}" ]; then
|
||||
echo "[Error] Didn't specify a filename";
|
||||
fi
|
||||
if [ -z "${BUNDLE_ID}" ]; then
|
||||
echo "[Error] Didn't specify bundle identifier";
|
||||
fi
|
||||
if [ -z "${PROVIDER}" ]; then
|
||||
echo "[Error] Didn't specify access provider";
|
||||
fi
|
||||
if [ -z "${USERNAME}" ]; then
|
||||
echo "[Error] Apple ID username is required";
|
||||
fi
|
||||
if [ -z "${PASSWORD}" ]; then
|
||||
echo "[Error] App Specific password is required";
|
||||
fi
|
||||
|
||||
if [ -z "${APP_NAME}" ] || [ -z "${USERNAME}" ] || [ -z "${BUNDLE_ID}" ] || [ -z "${PASSWORD}" ] || [ -z "${PROVIDER}" ]; then
|
||||
echo
|
||||
notarizeHelp
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
case "$APP_NAME" in
|
||||
*.app) sign;;
|
||||
*.zip) notarizationUpload "$APP_NAME";;
|
||||
*.dmg) notarizationUpload "$APP_NAME";;
|
||||
*.pkg) notarizationUpload "$APP_NAME";;
|
||||
esac
|
||||
|
||||
|
||||
}
|
||||
|
||||
notarizationUpload()
|
||||
{
|
||||
echo "[INFO] Uploading $APP_NAME for notarization"
|
||||
xcrun altool --notarize-app -t osx -f "$1" --primary-bundle-id "$BUNDLE_ID" -u "$USERNAME" -p "$PASSWORD" --asc-provider "$PROVIDER" --output-format xml
|
||||
|
||||
}
|
||||
|
||||
do_staple()
|
||||
{
|
||||
|
||||
if [ -z "${APP_NAME}" ]; then
|
||||
echo "[Error] Didn't specify a filename";
|
||||
echo
|
||||
stapleHelp
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[INFO] Stapling $APP_NAME"
|
||||
xcrun stapler staple "$APP_NAME"
|
||||
echo "[INFO] Validating Staple for $APP_NAME"
|
||||
xcrun stapler validate "$APP_NAME"
|
||||
}
|
||||
|
||||
|
||||
|
||||
#Excute Action base on the option -s -n -c
|
||||
case $ACTION in
|
||||
STAPLE) do_staple;;
|
||||
CHECK) do_check;;
|
||||
NOTARIZE) notarize;;
|
||||
*) helpFunction;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
unset APP_NAME ACTION SIGNING_IDENTITY BUNDLE_ID ENTITLEMENTS USERNAME PASSWORD PROVIDER UUID
|
||||
38
src/Importer/App.xaml.cs
Normal file
@ -0,0 +1,38 @@
|
||||
namespace Bit.Importer;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public App()
|
||||
{
|
||||
// Uncomment below to force light theme
|
||||
// Current.UserAppTheme = AppTheme.Light;
|
||||
InitializeComponent();
|
||||
|
||||
MainPage = new AppShell();
|
||||
}
|
||||
|
||||
protected override Window CreateWindow(IActivationState activationState)
|
||||
{
|
||||
var window = base.CreateWindow(activationState);
|
||||
window.Width = 650;
|
||||
window.Height = 1150;
|
||||
window.Title = "Bitwarden Importer";
|
||||
|
||||
if (DeviceInfo.Platform != DevicePlatform.WinUI)
|
||||
{
|
||||
window.MinimumWidth = 650;
|
||||
window.MinimumHeight = 1150;
|
||||
|
||||
Dispatcher.DispatchAsync(async () =>
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
window.MinimumWidth = 0;
|
||||
window.MinimumHeight = 0;
|
||||
window.MaximumWidth = double.PositiveInfinity;
|
||||
window.MaximumHeight = double.PositiveInfinity;
|
||||
});
|
||||
}
|
||||
|
||||
return window;
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@
|
||||
<UseMaui>true</UseMaui>
|
||||
<SingleProject>true</SingleProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseInterpreter>true</UseInterpreter>
|
||||
<DefaultLanguage>en</DefaultLanguage>
|
||||
<AssemblyName>Bitwarden Importer</AssemblyName>
|
||||
|
||||
@ -19,8 +20,8 @@
|
||||
<ApplicationIdGuid>c5a31c67-9745-473c-b2ac-cd797bf4615b</ApplicationIdGuid>
|
||||
|
||||
<!-- Versions -->
|
||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>1</ApplicationVersion>
|
||||
<ApplicationDisplayVersion>1.4</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>5</ApplicationVersion>
|
||||
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">13.1</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
|
||||
@ -32,6 +33,25 @@
|
||||
<WindowsAppSdkDeploymentManagerInitialize>false</WindowsAppSdkDeploymentManagerInitialize>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net7.0-maccatalyst|AnyCPU'">
|
||||
<CodesignProvision>Bitwarden Importer Developer App No Cat</CodesignProvision>
|
||||
<CodesignKey>Developer ID Application: Bitwarden Inc (LTZ2PFU5D6)</CodesignKey>
|
||||
<PackageSigningKey>Developer ID Installer: Bitwarden Inc (LTZ2PFU5D6)</PackageSigningKey>
|
||||
<ProvisionType>Manual</ProvisionType>
|
||||
<EnableCodeSigning>true</EnableCodeSigning>
|
||||
<CodesignEntitlements>Platforms\MacCatalyst\Entitlements.plist</CodesignEntitlements>
|
||||
<UseHardenedRuntime>true</UseHardenedRuntime>
|
||||
<CreatePackage>true</CreatePackage>
|
||||
<EnablePackageSigning>true</EnablePackageSigning>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net7.0-maccatalyst|AnyCPU'">
|
||||
<CreatePackage>false</CreatePackage>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- App Icon -->
|
||||
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#175DDC" />
|
||||
@ -51,14 +71,12 @@
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\Images\icon.svg" />
|
||||
<None Remove="Resources\Raw\bw-cli\bw-mac" />
|
||||
<None Remove="Resources\Raw\bw-cli\bw-windows.exe" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
|
||||
<PackageReference Include="PasswordManagerAccess" Version="10.2.1" />
|
||||
<PackageReference Include="PasswordManagerAccess" Version="12.0.0" />
|
||||
<PackageReference Include="ServiceStack.Text" Version="6.7.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
249
src/Importer/MainPage.xaml
Normal file
@ -0,0 +1,249 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.Importer.MainPage">
|
||||
|
||||
<ScrollView>
|
||||
<VerticalStackLayout
|
||||
Spacing="50"
|
||||
Padding="20"
|
||||
VerticalOptions="Start">
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="10"
|
||||
Padding="0">
|
||||
<Label
|
||||
Text="The Bitwarden Importer Tool helps you easily move all of your data from an existing password management service into your Bitwarden account. Simply input your credentials for Bitwarden and your old password management service into the form below and click the 'Import' button." />
|
||||
<Label
|
||||
Text="Click here to learn more"
|
||||
TextColor="{StaticResource Primary}"
|
||||
FontAttributes="Bold"
|
||||
TextDecorations="Underline"
|
||||
x:Name="LearnMore" />
|
||||
</VerticalStackLayout>
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="20"
|
||||
Padding="0">
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<Label
|
||||
Text="Bitwarden Server Url" />
|
||||
<Entry
|
||||
x:Name="BitwardenServerUrl" />
|
||||
</VerticalStackLayout>
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0"
|
||||
x:Name="BitwardenEmailLayout">
|
||||
<Label
|
||||
Text="Bitwarden Email" />
|
||||
<Entry
|
||||
x:Name="BitwardenEmail" />
|
||||
</VerticalStackLayout>
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="20"
|
||||
Padding="0"
|
||||
x:Name="BitwardenApiKeyLayout"
|
||||
IsVisible="false">
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<HorizontalStackLayout
|
||||
Spacing="10">
|
||||
<Label
|
||||
Text="Bitwarden API Key client_id" />
|
||||
<Label
|
||||
Text="Click here to get your API key"
|
||||
TextColor="{StaticResource Primary}"
|
||||
FontAttributes="Bold"
|
||||
TextDecorations="Underline"
|
||||
x:Name="ApiKeyLink1" />
|
||||
</HorizontalStackLayout>
|
||||
<Entry
|
||||
x:Name="BitwardenApiKeyClientId" />
|
||||
</VerticalStackLayout>
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<HorizontalStackLayout
|
||||
Spacing="10">
|
||||
<Label
|
||||
Text="Bitwarden API Key secret" />
|
||||
<Label
|
||||
Text="Click here to get your API key"
|
||||
TextColor="{StaticResource Primary}"
|
||||
FontAttributes="Bold"
|
||||
TextDecorations="Underline"
|
||||
x:Name="ApiKeyLink2"/>
|
||||
</HorizontalStackLayout>
|
||||
<Entry
|
||||
x:Name="BitwardenApiKeySecret"
|
||||
IsPassword="True" />
|
||||
</VerticalStackLayout>
|
||||
</VerticalStackLayout>
|
||||
|
||||
<HorizontalStackLayout>
|
||||
<CheckBox
|
||||
x:Name="BitwardenApiKeyOption"
|
||||
CheckedChanged="BitwardenApiKeyOption_CheckedChanged"
|
||||
VerticalOptions="Center" />
|
||||
<Label
|
||||
Text="Log in using API key instead."
|
||||
VerticalOptions="Center" />
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0"
|
||||
x:Name="BitwardenPasswordLayout">
|
||||
<Label
|
||||
Text="Bitwarden Master Password" />
|
||||
<Entry
|
||||
x:Name="BitwardenPassword"
|
||||
IsPassword="True" />
|
||||
</VerticalStackLayout>
|
||||
<HorizontalStackLayout
|
||||
x:Name="BitwardenKeyConnectorLayout">
|
||||
<CheckBox
|
||||
x:Name="BitwardenKeyConnector"
|
||||
CheckedChanged="BitwardenKeyConnector_CheckedChanged"
|
||||
VerticalOptions="Center" />
|
||||
<Label
|
||||
x:Name="BitwardenKeyConnectorLabel"
|
||||
Text="My organization uses a SSO configuration that does not require a master password."
|
||||
VerticalOptions="Center" />
|
||||
</HorizontalStackLayout>
|
||||
</VerticalStackLayout>
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="20"
|
||||
Padding="0">
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<Label
|
||||
x:Name="ServiceLabel"
|
||||
Text="Import From Service" />
|
||||
<Picker
|
||||
x:Name="Service"
|
||||
SelectedIndexChanged="Service_SelectedIndexChanged" />
|
||||
</VerticalStackLayout>
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="20"
|
||||
Padding="0"
|
||||
x:Name="LastPassLayout">
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<Label
|
||||
Text="LastPass Email" />
|
||||
<Entry
|
||||
x:Name="LastPassEmail" />
|
||||
</VerticalStackLayout>
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<Label
|
||||
Text="LastPass Master Password" />
|
||||
<Entry
|
||||
x:Name="LastPassPassword"
|
||||
IsPassword="True" />
|
||||
</VerticalStackLayout>
|
||||
|
||||
<HorizontalStackLayout>
|
||||
<CheckBox
|
||||
x:Name="LastPassSkipShared"
|
||||
VerticalOptions="Center" />
|
||||
<Label
|
||||
x:Name="LastPassSkipSharedLabel"
|
||||
Text="Skip items from shared folders"
|
||||
VerticalOptions="Center" />
|
||||
</HorizontalStackLayout>
|
||||
</VerticalStackLayout>
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="20"
|
||||
Padding="0"
|
||||
x:Name="OnePasswordLayout"
|
||||
IsVisible="false">
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<Label
|
||||
Text="1Password Email" />
|
||||
<Entry
|
||||
x:Name="OnePasswordEmail" />
|
||||
</VerticalStackLayout>
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<Label
|
||||
Text="1Password Secret Key" />
|
||||
<Entry
|
||||
x:Name="OnePasswordSecretKey" />
|
||||
</VerticalStackLayout>
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<Label
|
||||
Text="1Password Password" />
|
||||
<Entry
|
||||
x:Name="OnePasswordPassword"
|
||||
IsPassword="True" />
|
||||
</VerticalStackLayout>
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<Label
|
||||
Text="1Password Domain" />
|
||||
<Entry
|
||||
x:Name="OnePasswordDomain"
|
||||
Text="my.1password.com" />
|
||||
</VerticalStackLayout>
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<Label
|
||||
Text="1Password Excluded Vault Names (CSV)" />
|
||||
<Entry
|
||||
x:Name="OnePasswordExcludedVaults" />
|
||||
</VerticalStackLayout>
|
||||
</VerticalStackLayout>
|
||||
</VerticalStackLayout>
|
||||
|
||||
<HorizontalStackLayout
|
||||
Spacing="10"
|
||||
Padding="0">
|
||||
<Button
|
||||
x:Name="SubmitButton"
|
||||
Text="Import"
|
||||
Clicked="OnButtonClicked"
|
||||
HorizontalOptions="Center" />
|
||||
<ActivityIndicator
|
||||
x:Name="Loading"
|
||||
IsRunning="false"
|
||||
VerticalOptions="Center" />
|
||||
<Label
|
||||
x:Name="PleaseWait"
|
||||
Text="Please wait..."
|
||||
IsVisible="false"
|
||||
VerticalOptions="Center" />
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<Label
|
||||
x:Name="CachePath"
|
||||
IsVisible="false" />
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</ContentPage>
|
||||
662
src/Importer/MainPage.xaml.cs
Normal file
@ -0,0 +1,662 @@
|
||||
using Bit.Importer.Services;
|
||||
using Bit.Importer.Services.LastPass;
|
||||
using Bit.Importer.Services.OnePassword;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Bit.Importer;
|
||||
|
||||
public partial class MainPage : ContentPage
|
||||
{
|
||||
private readonly HttpClient _httpClient = new();
|
||||
private readonly bool _doLogging = false;
|
||||
private readonly string _cacheDir;
|
||||
private readonly List<string> _services = new() { "LastPass"/*, "1Password"*/ };
|
||||
private readonly string _cliVersion = "2023.4.0";
|
||||
private readonly string _cliBaseDownloadUrl = "https://assets.bitwarden.com/importer";
|
||||
private readonly string _bitwardenCloudUrl = "https://bitwarden.com";
|
||||
private readonly Dictionary<string, int> _twoFactorMethods =
|
||||
new() { { "Authenticator app", 0 }, { "Email", 1 }, { "YubiKey OTP security key", 3 } };
|
||||
|
||||
public MainPage()
|
||||
{
|
||||
_cacheDir = Path.Combine(FileSystem.CacheDirectory, "com.bitwarden.importer");
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
BitwardenServerUrl.Text = _bitwardenCloudUrl;
|
||||
Service.ItemsSource = _services;
|
||||
Service.SelectedIndex = 0;
|
||||
Service.IsVisible = ServiceLabel.IsVisible = _services.Count > 1;
|
||||
|
||||
var learnMoreTap = new TapGestureRecognizer();
|
||||
learnMoreTap.Tapped += async (s, e) =>
|
||||
{
|
||||
await Browser.Default.OpenAsync(new Uri("https://bitwarden.com/help/bitwarden-importer-tool/"),
|
||||
BrowserLaunchMode.SystemPreferred);
|
||||
};
|
||||
LearnMore.GestureRecognizers.Add(learnMoreTap);
|
||||
|
||||
var apiKeyTap = new TapGestureRecognizer();
|
||||
apiKeyTap.Tapped += async (s, e) =>
|
||||
{
|
||||
await Browser.Default.OpenAsync(new Uri("https://vault.bitwarden.com/#/settings/security/security-keys"),
|
||||
BrowserLaunchMode.SystemPreferred);
|
||||
};
|
||||
ApiKeyLink1.GestureRecognizers.Add(apiKeyTap);
|
||||
ApiKeyLink2.GestureRecognizers.Add(apiKeyTap);
|
||||
|
||||
CachePath.Text = _cacheDir;
|
||||
ParseCommandlineDefaults();
|
||||
}
|
||||
|
||||
private void BitwardenKeyConnector_CheckedChanged(object sender, CheckedChangedEventArgs e)
|
||||
{
|
||||
// We don't need master passwqord if using key connector
|
||||
BitwardenPasswordLayout.IsVisible = !BitwardenKeyConnector.IsChecked;
|
||||
}
|
||||
|
||||
private void BitwardenApiKeyOption_CheckedChanged(object sender, CheckedChangedEventArgs e)
|
||||
{
|
||||
BitwardenApiKeyLayout.IsVisible = BitwardenApiKeyOption.IsChecked;
|
||||
BitwardenEmailLayout.IsVisible = !BitwardenApiKeyOption.IsChecked;
|
||||
}
|
||||
|
||||
private async void OnButtonClicked(object sender, EventArgs e)
|
||||
{
|
||||
// Validate
|
||||
if (!await ValidateInputsAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Start loading state
|
||||
Loading.IsRunning = true;
|
||||
SubmitButton.IsEnabled = false;
|
||||
PleaseWait.IsVisible = true;
|
||||
|
||||
// Run the import task
|
||||
await Task.Run(ImportAsync);
|
||||
}
|
||||
|
||||
private void Service_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
LastPassLayout.IsVisible = _services[Service.SelectedIndex] == "LastPass";
|
||||
OnePasswordLayout.IsVisible = _services[Service.SelectedIndex] == "1Password";
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateInputsAsync()
|
||||
{
|
||||
if (BitwardenApiKeyOption.IsChecked &&
|
||||
(string.IsNullOrWhiteSpace(BitwardenApiKeyClientId?.Text) ||
|
||||
string.IsNullOrWhiteSpace(BitwardenApiKeySecret?.Text)))
|
||||
{
|
||||
await DisplayAlert("Error", "Bitwarden API key information is required.", "OK");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!BitwardenApiKeyOption.IsChecked &&
|
||||
string.IsNullOrWhiteSpace(BitwardenEmail?.Text))
|
||||
{
|
||||
await DisplayAlert("Error", "Bitwarden email is required.", "OK");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (BitwardenKeyConnector.IsChecked && !BitwardenApiKeyOption.IsChecked)
|
||||
{
|
||||
await DisplayAlert("Error", "Bitwarden APIs keys are required when your organization uses SSO.", "OK");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(BitwardenPassword?.Text) &&
|
||||
!BitwardenKeyConnector.IsChecked)
|
||||
{
|
||||
await DisplayAlert("Error", "Bitwarden master password is required.", "OK");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_services[Service.SelectedIndex] == "LastPass")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(LastPassEmail?.Text) ||
|
||||
string.IsNullOrWhiteSpace(LastPassPassword?.Text))
|
||||
{
|
||||
await DisplayAlert("Error", "LastPass Email and Master Password are required.", "OK");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (_services[Service.SelectedIndex] == "1Password")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(OnePasswordEmail?.Text) ||
|
||||
string.IsNullOrWhiteSpace(OnePasswordSecretKey?.Text) ||
|
||||
string.IsNullOrWhiteSpace(OnePasswordDomain?.Text) ||
|
||||
string.IsNullOrWhiteSpace(OnePasswordPassword?.Text))
|
||||
{
|
||||
await DisplayAlert("Error", "1Password information is required.", "OK");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task ImportAsync()
|
||||
{
|
||||
await SetupAsync();
|
||||
await CleanupAsync();
|
||||
|
||||
IImportService importService = null;
|
||||
var serviceSelection = _services[Service.SelectedIndex];
|
||||
if (serviceSelection == "LastPass")
|
||||
{
|
||||
importService = new LastPassImportService(this, _cacheDir,
|
||||
LastPassEmail?.Text, LastPassPassword?.Text, LastPassSkipShared.IsChecked);
|
||||
}
|
||||
else if (serviceSelection == "1Password")
|
||||
{
|
||||
importService = new OnePasswordImportService(this, _cacheDir,
|
||||
OnePasswordEmail?.Text, OnePasswordSecretKey?.Text, OnePasswordPassword?.Text,
|
||||
OnePasswordDomain?.Text, OnePasswordExcludedVaults.Text);
|
||||
}
|
||||
|
||||
var (serviceSuccess, importFilePath, importOption) = (false, string.Empty, string.Empty);
|
||||
if (importService != null)
|
||||
{
|
||||
(serviceSuccess, importFilePath, importOption) = await importService.CreateImportFileAsync();
|
||||
}
|
||||
|
||||
if (!serviceSuccess)
|
||||
{
|
||||
StopLoadingAndAlert(true,
|
||||
$"Unable to log into your {serviceSelection} account. Are your credentials correct?");
|
||||
}
|
||||
|
||||
var cliSetupSuccess = false;
|
||||
try
|
||||
{
|
||||
if (serviceSuccess)
|
||||
{
|
||||
await SetupCliAsync();
|
||||
cliSetupSuccess = true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
StopLoadingAndAlert(true, "Unable to set up Bitwarden CLI.");
|
||||
}
|
||||
|
||||
if (cliSetupSuccess && serviceSuccess)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(BitwardenServerUrl?.Text) && BitwardenServerUrl.Text != "https://bitwarden.com")
|
||||
{
|
||||
var configServerSuccess = ConfigServerCli();
|
||||
if (!configServerSuccess)
|
||||
{
|
||||
StopLoadingAndAlert(true, "Unable to configure Bitwarden server.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var (loginSuccess, sessionKey) = LogInCli();
|
||||
if (!loginSuccess)
|
||||
{
|
||||
if (BitwardenApiKeyOption.IsChecked)
|
||||
{
|
||||
StopLoadingAndAlert(true,
|
||||
"Unable to log into your Bitwarden account. Is your API key information correct?");
|
||||
}
|
||||
else
|
||||
{
|
||||
StopLoadingAndAlert(true,
|
||||
"Unable to log into your Bitwarden account. " +
|
||||
"Try logging in using the API key option instead.");
|
||||
}
|
||||
}
|
||||
|
||||
if (loginSuccess && string.IsNullOrWhiteSpace(sessionKey))
|
||||
{
|
||||
var (unlockSuccess, unlockSessionKey) = UnlockCli();
|
||||
sessionKey = unlockSessionKey;
|
||||
if (!unlockSuccess)
|
||||
{
|
||||
StopLoadingAndAlert(true,
|
||||
"Unable to unlock your Bitwarden vault. Is your master password correct?");
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(importFilePath) && !string.IsNullOrWhiteSpace(sessionKey))
|
||||
{
|
||||
var importSuccess = ImportCli(importOption, importFilePath, sessionKey);
|
||||
if (importSuccess)
|
||||
{
|
||||
StopLoadingAndAlert(false, "Your import was successful!");
|
||||
ClearInputs();
|
||||
}
|
||||
else
|
||||
{
|
||||
StopLoadingAndAlert(true, "Something went wrong with the import.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await CleanupAsync();
|
||||
}
|
||||
|
||||
private void StopLoadingAndAlert(bool error, string message)
|
||||
{
|
||||
Dispatcher.Dispatch(async () =>
|
||||
{
|
||||
// Stop the loading state
|
||||
Loading.IsRunning = false;
|
||||
SubmitButton.IsEnabled = true;
|
||||
PleaseWait.IsVisible = false;
|
||||
|
||||
// Show alert
|
||||
if (error)
|
||||
{
|
||||
await DisplayAlert("Error", message, "OK");
|
||||
}
|
||||
else
|
||||
{
|
||||
await DisplayAlert("Success", message, "OK");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private bool ConfigServerCli()
|
||||
{
|
||||
var (exitCode, stdOut, stdErr) = ExecCli($"config server {BitwardenServerUrl?.Text}");
|
||||
return exitCode == 0;
|
||||
}
|
||||
|
||||
private (bool, string) LogInCli()
|
||||
{
|
||||
var (exitCode, sessionKey, error) = (1, string.Empty, string.Empty);
|
||||
// API key login
|
||||
if (BitwardenApiKeyOption.IsChecked)
|
||||
{
|
||||
(exitCode, sessionKey, error) = ExecCli("login --apikey --raw", (process) =>
|
||||
{
|
||||
process.StartInfo.EnvironmentVariables["BW_CLIENTID"] = BitwardenApiKeyClientId?.Text;
|
||||
process.StartInfo.EnvironmentVariables["BW_CLIENTSECRET"] = BitwardenApiKeySecret?.Text;
|
||||
// Avoid BW_NOINTERACTION bug that is issuing invalid session key on api key login
|
||||
process.StartInfo.EnvironmentVariables["BW_NOINTERACTION"] = "false";
|
||||
});
|
||||
}
|
||||
// Master password login
|
||||
else
|
||||
{
|
||||
var command = string.Format("login {0} {1} --raw", BitwardenEmail?.Text, BitwardenPassword?.Text);
|
||||
(exitCode, sessionKey, error) = ExecCli(command);
|
||||
if (exitCode == 1)
|
||||
{
|
||||
var promptMethod = error.Contains("no provider selected", StringComparison.CurrentCultureIgnoreCase);
|
||||
var promptCode = promptMethod || error.Contains("code is required", StringComparison.CurrentCultureIgnoreCase);
|
||||
int? method = null;
|
||||
string code = null;
|
||||
|
||||
if (promptMethod)
|
||||
{
|
||||
var methodTask = Dispatcher.DispatchAsync(() =>
|
||||
DisplayActionSheet("Select Bitwarden 2FA method.", "Cancel", null,
|
||||
_twoFactorMethods.Keys.ToArray()));
|
||||
var methodKey = methodTask.GetAwaiter().GetResult();
|
||||
if (!string.IsNullOrWhiteSpace(methodKey))
|
||||
{
|
||||
method = _twoFactorMethods[methodKey];
|
||||
}
|
||||
}
|
||||
|
||||
if (promptCode)
|
||||
{
|
||||
var codeTask = Dispatcher.DispatchAsync(() =>
|
||||
DisplayPromptAsync("Bitwarden Two-step Login", "Enter your two-step login code.", "Submit"));
|
||||
code = codeTask.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
if (method.HasValue)
|
||||
{
|
||||
command = string.Format("login {0} {1} --method {2} --code {3} --raw",
|
||||
BitwardenEmail?.Text, BitwardenPassword?.Text, method.Value, code);
|
||||
}
|
||||
else
|
||||
{
|
||||
command = string.Format("login {0} {1} --code {2} --raw",
|
||||
BitwardenEmail?.Text, BitwardenPassword?.Text, code);
|
||||
}
|
||||
|
||||
(exitCode, sessionKey, error) = ExecCli(command);
|
||||
}
|
||||
}
|
||||
return (exitCode == 0, sessionKey);
|
||||
}
|
||||
|
||||
private (bool, string) UnlockCli()
|
||||
{
|
||||
var (exitCode, sessionKey, error) = ExecCli($"unlock {BitwardenPassword?.Text} --raw");
|
||||
return (exitCode == 0, sessionKey);
|
||||
}
|
||||
|
||||
private bool ImportCli(string importService, string importFilePath, string sessionKey)
|
||||
{
|
||||
var (importExitCode, importStdOut, stdErr) = ExecCli($"import {importService} {importFilePath}", (process) =>
|
||||
{
|
||||
process.StartInfo.EnvironmentVariables["BW_SESSION"] = sessionKey;
|
||||
});
|
||||
return importExitCode == 0 && (importStdOut?.Contains("Imported") ?? false);
|
||||
}
|
||||
|
||||
private async Task SetupCliAsync()
|
||||
{
|
||||
if (await HasLatestCliAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Download CLI app to disk so that we can invoke it.
|
||||
|
||||
var isWindows = DeviceInfo.Platform == DevicePlatform.WinUI;
|
||||
|
||||
// Hash file
|
||||
var cliHashUrl = string.Format(
|
||||
"{0}/cli-v{1}/bw-{2}-sha256-{1}.txt",
|
||||
_cliBaseDownloadUrl, _cliVersion, isWindows ? "windows" : "macos");
|
||||
var cliHashFilename = Path.Combine(_cacheDir, "bw.sha256.txt");
|
||||
await DownloadFileAsync(cliHashUrl, cliHashFilename);
|
||||
|
||||
// Zip file
|
||||
var cliUrl = string.Format("{0}/cli-v{1}/bw-{2}-{1}.zip",
|
||||
_cliBaseDownloadUrl, _cliVersion, isWindows ? "windows" : "macos");
|
||||
var cliZipFilename = Path.Combine(_cacheDir, "bw.zip");
|
||||
var cliPath = ResolveCliPath();
|
||||
await DownloadFileAsync(cliUrl, cliZipFilename);
|
||||
|
||||
// Verify checksums
|
||||
using var hashFileStream = File.OpenRead(cliHashFilename);
|
||||
using var hashFileReader = new StreamReader(hashFileStream);
|
||||
var hashFileHex = hashFileReader.ReadToEnd().Trim();
|
||||
hashFileStream.Close();
|
||||
|
||||
using var zipStream = File.OpenRead(cliZipFilename);
|
||||
using var sha256 = SHA256.Create();
|
||||
var zipHashBytes = sha256.ComputeHash(zipStream);
|
||||
var zipHashHex = BitConverter.ToString(zipHashBytes).Replace("-", string.Empty);
|
||||
zipStream.Close();
|
||||
|
||||
if (!string.Equals(zipHashHex, hashFileHex, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
throw new Exception("CLI checksum failed.");
|
||||
}
|
||||
|
||||
// Extract zip
|
||||
ZipFile.ExtractToDirectory(cliZipFilename, _cacheDir, true);
|
||||
|
||||
// macOS permissions
|
||||
if (!isWindows)
|
||||
{
|
||||
ExecBash($"chmod +x {cliPath}");
|
||||
}
|
||||
}
|
||||
|
||||
private (int, string, string) ExecCli(string args, Action<Process> processAction = null)
|
||||
{
|
||||
// Set up the process
|
||||
using var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = ResolveCliPath(),
|
||||
Arguments = args,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
},
|
||||
};
|
||||
|
||||
// Load standard env vars for this use case
|
||||
process.StartInfo.EnvironmentVariables["BITWARDENCLI_APPDATA_DIR"] = _cacheDir;
|
||||
process.StartInfo.EnvironmentVariables["BW_NOINTERACTION"] = "true";
|
||||
processAction?.Invoke(process);
|
||||
|
||||
process.Start();
|
||||
var stdOut = "";
|
||||
var stdErr = "";
|
||||
while (!process.StandardOutput.EndOfStream)
|
||||
{
|
||||
stdOut += process.StandardOutput.ReadLine();
|
||||
}
|
||||
while (!process.StandardError.EndOfStream)
|
||||
{
|
||||
stdErr += process.StandardError.ReadLine();
|
||||
}
|
||||
process.StandardOutput.Close();
|
||||
process.WaitForExit();
|
||||
return (process.ExitCode, stdOut.Trim(), stdErr.Trim());
|
||||
}
|
||||
|
||||
private string ResolveCliPath()
|
||||
{
|
||||
var bwCliFilename = DeviceInfo.Platform == DevicePlatform.WinUI ? "bw.exe" : "bw";
|
||||
return Path.Combine(_cacheDir, bwCliFilename);
|
||||
}
|
||||
|
||||
private Task CleanupAsync()
|
||||
{
|
||||
File.Delete(Path.Combine(_cacheDir, "data.json"));
|
||||
File.Delete(Path.Combine(_cacheDir, "export.csv"));
|
||||
File.Delete(Path.Combine(_cacheDir, "bw.zip"));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
private Task SetupAsync()
|
||||
{
|
||||
if (!Directory.Exists(_cacheDir))
|
||||
{
|
||||
Directory.CreateDirectory(_cacheDir);
|
||||
}
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public static void ExecBash(string cmd)
|
||||
{
|
||||
var escapedArgs = cmd.Replace("\"", "\\\"");
|
||||
using var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
FileName = "/bin/bash",
|
||||
Arguments = $"-c \"{escapedArgs}\""
|
||||
}
|
||||
};
|
||||
process.Start();
|
||||
process.WaitForExit();
|
||||
}
|
||||
|
||||
private void ClearInputs()
|
||||
{
|
||||
Dispatcher.Dispatch(() =>
|
||||
{
|
||||
BitwardenServerUrl.Text = string.Empty;
|
||||
BitwardenEmail.Text = string.Empty;
|
||||
BitwardenApiKeyOption.IsChecked = false;
|
||||
BitwardenApiKeyClientId.Text = string.Empty;
|
||||
BitwardenApiKeySecret.Text = string.Empty;
|
||||
BitwardenKeyConnector.IsChecked = false;
|
||||
BitwardenPassword.Text = string.Empty;
|
||||
LastPassEmail.Text = string.Empty;
|
||||
LastPassPassword.Text = string.Empty;
|
||||
OnePasswordEmail.Text = string.Empty;
|
||||
OnePasswordPassword.Text = string.Empty;
|
||||
OnePasswordSecretKey.Text = string.Empty;
|
||||
OnePasswordDomain.Text = string.Empty;
|
||||
OnePasswordExcludedVaults.Text = string.Empty;
|
||||
});
|
||||
}
|
||||
|
||||
private void ParseCommandlineDefaults()
|
||||
{
|
||||
var args = Environment.GetCommandLineArgs();
|
||||
foreach (var arg in args)
|
||||
{
|
||||
if (!arg.Contains('='))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var argParts = arg.Split(new[] { '=' }, 2);
|
||||
if (argParts.Length < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "bitwardenServerUrl")
|
||||
{
|
||||
BitwardenServerUrl.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "bitwardenEmail")
|
||||
{
|
||||
BitwardenEmail.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "bitwardenApiKey")
|
||||
{
|
||||
BitwardenApiKeyOption.IsChecked = argParts[1] == "1";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "bitwardenApiKeyClientId")
|
||||
{
|
||||
BitwardenApiKeyClientId.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "bitwardenApiKeySecret")
|
||||
{
|
||||
BitwardenApiKeySecret.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "bitwardenMasterPassword")
|
||||
{
|
||||
BitwardenPassword.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "bitwardenKeyConnector")
|
||||
{
|
||||
BitwardenKeyConnector.IsChecked = argParts[1] == "1";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "lastpassEmail")
|
||||
{
|
||||
LastPassEmail.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "lastpassMasterPassword")
|
||||
{
|
||||
LastPassPassword.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "lastpassSkipShared")
|
||||
{
|
||||
LastPassSkipShared.IsChecked = argParts[1] == "1";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "disableLastpassSkipShared")
|
||||
{
|
||||
LastPassSkipShared.IsEnabled = argParts[1] != "1";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "1passwordEmail")
|
||||
{
|
||||
OnePasswordEmail.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "1passwordPassword")
|
||||
{
|
||||
OnePasswordPassword.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "1passwordSecretKey")
|
||||
{
|
||||
OnePasswordSecretKey.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "1passwordDomain")
|
||||
{
|
||||
OnePasswordDomain.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argParts[0] == "1passwordExcludedVaults")
|
||||
{
|
||||
OnePasswordExcludedVaults.Text = argParts[1];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Log(string message)
|
||||
{
|
||||
if (_doLogging)
|
||||
{
|
||||
File.AppendAllText(Path.Combine(_cacheDir, "log.txt"),
|
||||
$"[{DateTime.UtcNow}] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> HasLatestCliAsync()
|
||||
{
|
||||
var cliHashFilename = Path.Combine(_cacheDir, "bw.sha256.txt");
|
||||
if (!File.Exists(cliHashFilename) || !File.Exists(ResolveCliPath()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read zip hash file on disk
|
||||
using var zipHashStream = File.OpenRead(cliHashFilename);
|
||||
using var zipHashReader = new StreamReader(zipHashStream);
|
||||
var zipHashHex = zipHashReader.ReadToEnd().Trim();
|
||||
|
||||
// Download hash from latest CLI release
|
||||
var cliHashUrl = string.Format(
|
||||
"{0}/cli-v{1}/bw-{2}-sha256-{1}.txt",
|
||||
_cliBaseDownloadUrl, _cliVersion, DeviceInfo.Platform == DevicePlatform.WinUI ? "windows" : "macos");
|
||||
using var hashStream = await _httpClient.GetStreamAsync(cliHashUrl);
|
||||
using var reader = new StreamReader(hashStream);
|
||||
var hashHex = reader.ReadToEnd().Trim();
|
||||
|
||||
// Close streams
|
||||
zipHashStream.Close();
|
||||
hashStream.Close();
|
||||
|
||||
// Compare the hashes
|
||||
return string.Equals(zipHashHex, hashHex, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
private async Task DownloadFileAsync(string url, string file)
|
||||
{
|
||||
using var stream = await _httpClient.GetStreamAsync(url);
|
||||
using var fileStream = File.Create(file);
|
||||
await stream.CopyToAsync(fileStream);
|
||||
stream.Close();
|
||||
fileStream.Close();
|
||||
}
|
||||
}
|
||||
12
src/Importer/Platforms/MacCatalyst/Entitlements.plist
Normal file
@ -0,0 +1,12 @@
|
||||
<?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>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -6,7 +6,7 @@
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap">
|
||||
|
||||
<Identity Name="bitwarden-importer" Publisher="CN=8bit Solutions LLC, O=8bit Solutions LLC, L=Jacksonville, S=Florida, C=US, SERIALNUMBER=L16000106119, OID.1.3.6.1.4.1.311.60.2.1.2=Florida, OID.1.3.6.1.4.1.311.60.2.1.3=US, OID.2.5.4.15=Private Organization" Version="1.0.0.0" />
|
||||
<Identity Name="bitwarden-importer" Publisher="CN=8bit Solutions LLC, O=8bit Solutions LLC, L=Jacksonville, S=Florida, C=US, SERIALNUMBER=L16000106119, OID.1.3.6.1.4.1.311.60.2.1.2=Florida, OID.1.3.6.1.4.1.311.60.2.1.3=US, OID.2.5.4.15=Private Organization" Version="1.5.0.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="569357BF-7B0D-4BDB-B134-0D3A5765BA50" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 228 B After Width: | Height: | Size: 228 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
41
src/Importer/Services/BaseUi.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using PasswordManagerAccess.Common;
|
||||
|
||||
namespace Bit.Importer.Services;
|
||||
|
||||
public class BaseUi : IDuoUi
|
||||
{
|
||||
private readonly MainPage _page;
|
||||
private readonly string _serviceName;
|
||||
|
||||
public BaseUi(MainPage page, string serviceName)
|
||||
{
|
||||
_page = page;
|
||||
_serviceName = serviceName;
|
||||
}
|
||||
|
||||
public DuoChoice ChooseDuoFactor(DuoDevice[] devices)
|
||||
{
|
||||
var task = _page.Dispatcher.DispatchAsync(() =>
|
||||
_page.DisplayActionSheet("Choose a Duo device", "Cancel", null, devices.Select(d => d.Name).ToArray()));
|
||||
var actionSelection = task.GetAwaiter().GetResult();
|
||||
var device = devices.FirstOrDefault(d => d.Name == actionSelection);
|
||||
return new DuoChoice(device, DuoFactor.Passcode, false);
|
||||
}
|
||||
|
||||
public string ProvideDuoPasscode(DuoDevice device)
|
||||
{
|
||||
return PromptCode("Enter your Duo passcode.");
|
||||
}
|
||||
|
||||
public void UpdateDuoStatus(DuoStatus status, string text)
|
||||
{
|
||||
// Not sure what this is for.
|
||||
}
|
||||
|
||||
protected string PromptCode(string message)
|
||||
{
|
||||
var task = _page.Dispatcher.DispatchAsync(() =>
|
||||
_page.DisplayPromptAsync($"{_serviceName} Two-step Login", message, "Submit"));
|
||||
return task.GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
6
src/Importer/Services/IImportService.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Bit.Importer.Services;
|
||||
|
||||
public interface IImportService
|
||||
{
|
||||
Task<(bool, string, string)> CreateImportFileAsync();
|
||||
}
|
||||
@ -1,8 +1,9 @@
|
||||
using CsvHelper.Configuration.Attributes;
|
||||
using PasswordManagerAccess.LastPass;
|
||||
using PasswordManagerAccess.LastPass;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Bit.Importer.Services.LastPass;
|
||||
|
||||
[DataContract]
|
||||
public class ExportedAccount
|
||||
{
|
||||
public ExportedAccount() { }
|
||||
@ -12,27 +13,27 @@ public class ExportedAccount
|
||||
Url = account.Url;
|
||||
Username = account.Username;
|
||||
Password = account.Password;
|
||||
// Totp not supported
|
||||
// Extra = account.Notes;
|
||||
Totp = account.Totp;
|
||||
Extra = account.Notes;
|
||||
Name = account.Name;
|
||||
Grouping = account.Path == "(none)" ? null : account.Path;
|
||||
// Fav = account.Favorite == "1" ? 1 : 0;
|
||||
Fav = account.IsFavorite ? 1 : 0;
|
||||
}
|
||||
|
||||
[Name("url")]
|
||||
[DataMember(Name = "url")]
|
||||
public string Url { get; set; }
|
||||
[Name("username")]
|
||||
[DataMember(Name = "username")]
|
||||
public string Username { get; set; }
|
||||
[Name("password")]
|
||||
[DataMember(Name = "password")]
|
||||
public string Password { get; set; }
|
||||
[Name("totp")]
|
||||
[DataMember(Name = "totp")]
|
||||
public string Totp { get; set; }
|
||||
[Name("extra")]
|
||||
[DataMember(Name = "extra")]
|
||||
public string Extra { get; set; }
|
||||
[Name("name")]
|
||||
[DataMember(Name = "name")]
|
||||
public string Name { get; set; }
|
||||
[Name("grouping")]
|
||||
[DataMember(Name = "grouping")]
|
||||
public string Grouping { get; set; }
|
||||
[Name("fav")]
|
||||
[DataMember(Name = "fav")]
|
||||
public int Fav { get; set; }
|
||||
}
|
||||
58
src/Importer/Services/LastPass/LastPassImportService.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using PasswordManagerAccess.LastPass;
|
||||
using ServiceStack.Text;
|
||||
|
||||
namespace Bit.Importer.Services.LastPass;
|
||||
|
||||
public class LastPassImportService : IImportService
|
||||
{
|
||||
private const string ImportOptionName = "lastpasscsv";
|
||||
|
||||
private readonly MainPage _page;
|
||||
private readonly string _cacheDir;
|
||||
private readonly string _email;
|
||||
private readonly string _password;
|
||||
private readonly bool _skipShared;
|
||||
|
||||
public LastPassImportService(MainPage page, string cacheDir,
|
||||
string email, string password, bool skipShared)
|
||||
{
|
||||
_page = page;
|
||||
_cacheDir = cacheDir;
|
||||
_email = email;
|
||||
_password = password;
|
||||
_skipShared = skipShared;
|
||||
}
|
||||
|
||||
public async Task<(bool, string, string)> CreateImportFileAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Log in and get LastPass data
|
||||
var ui = new Ui(_page);
|
||||
var clientInfo = new ClientInfo(
|
||||
PasswordManagerAccess.LastPass.Platform.Desktop,
|
||||
Guid.NewGuid().ToString().ToLower(),
|
||||
"Importer");
|
||||
var vault = Vault.Open(_email, _password, clientInfo, ui,
|
||||
new ParserOptions { ParseSecureNotesToAccount = false });
|
||||
|
||||
// Filter accounts
|
||||
var filteredAccounts = vault.Accounts.Where(a => !a.IsShared || (a.IsShared && !_skipShared));
|
||||
|
||||
// Massage it to expected CSV format
|
||||
var exportAccounts = filteredAccounts.Select(a => new ExportedAccount(a));
|
||||
|
||||
// Create CSV string
|
||||
var csvOutput = CsvSerializer.SerializeToCsv(exportAccounts);
|
||||
|
||||
// Write CSV to temp disk
|
||||
var csvPath = Path.Combine(_cacheDir, "export.csv");
|
||||
await File.WriteAllTextAsync(csvPath, csvOutput);
|
||||
return (true, csvPath, ImportOptionName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (false, null, ImportOptionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/Importer/Services/LastPass/Ui.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using PasswordManagerAccess.LastPass.Ui;
|
||||
|
||||
namespace Bit.Importer.Services.LastPass;
|
||||
|
||||
public class Ui : BaseUi, IUi
|
||||
{
|
||||
public Ui(MainPage page)
|
||||
: base(page, "LastPass") { }
|
||||
|
||||
public OtpResult ProvideGoogleAuthPasscode()
|
||||
{
|
||||
return new OtpResult(PromptCode("Enter your authenticator two-step login code."), false);
|
||||
}
|
||||
|
||||
public OtpResult ProvideMicrosoftAuthPasscode()
|
||||
{
|
||||
return new OtpResult(PromptCode("Enter your authenticator two-step login code."), false);
|
||||
}
|
||||
|
||||
public OtpResult ProvideYubikeyPasscode()
|
||||
{
|
||||
return new OtpResult(PromptCode("Enter your Yubikey code."), false);
|
||||
}
|
||||
|
||||
public OobResult ApproveLastPassAuth()
|
||||
{
|
||||
return OobResult.ContinueWithPasscode(PromptCode("Enter passcode from LastPass Authenticator."), false);
|
||||
}
|
||||
|
||||
public OobResult ApproveDuo()
|
||||
{
|
||||
return OobResult.ContinueWithPasscode(PromptCode("Enter passcode from Duo."), false);
|
||||
}
|
||||
|
||||
public OobResult ApproveSalesforceAuth()
|
||||
{
|
||||
return OobResult.ContinueWithPasscode(PromptCode("Enter passcode from Salesforce Authenticator."), false);
|
||||
}
|
||||
}
|
||||
23
src/Importer/Services/MemoryStorage.cs
Normal file
@ -0,0 +1,23 @@
|
||||
namespace Bit.Importer.Services;
|
||||
|
||||
public class MemoryStorage : PasswordManagerAccess.Common.ISecureStorage
|
||||
{
|
||||
private readonly Dictionary<string, string> _storage = new();
|
||||
|
||||
public void StoreString(string name, string value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
_storage.Remove(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
_storage[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string LoadString(string name)
|
||||
{
|
||||
return _storage.ContainsKey(name) ? _storage[name] : null;
|
||||
}
|
||||
}
|
||||
30
src/Importer/Services/OnePassword/ExportedAccount.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System.Runtime.Serialization;
|
||||
using PasswordManagerAccess.OnePassword;
|
||||
|
||||
namespace Bit.Importer.Services.OnePassword;
|
||||
|
||||
[DataContract]
|
||||
public class ExportedAccount
|
||||
{
|
||||
public ExportedAccount() { }
|
||||
|
||||
public ExportedAccount(Account account)
|
||||
{
|
||||
Title = account.Name;
|
||||
Urls = account.MainUrl;
|
||||
Username = account.Username;
|
||||
Password = account.Password;
|
||||
Notes = account.Note;
|
||||
}
|
||||
|
||||
[DataMember(Name = "title")]
|
||||
public string Title { get; set; }
|
||||
[DataMember(Name = "urls")]
|
||||
public string Urls { get; set; }
|
||||
[DataMember(Name = "username")]
|
||||
public string Username { get; set; }
|
||||
[DataMember(Name = "password")]
|
||||
public string Password { get; set; }
|
||||
[DataMember(Name = "notes")]
|
||||
public string Notes { get; set; }
|
||||
}
|
||||
100
src/Importer/Services/OnePassword/OnePasswordImportService.cs
Normal file
@ -0,0 +1,100 @@
|
||||
using PasswordManagerAccess.OnePassword;
|
||||
using ServiceStack.Text;
|
||||
|
||||
namespace Bit.Importer.Services.OnePassword;
|
||||
|
||||
internal class OnePasswordImportService : IImportService
|
||||
{
|
||||
private const string ImportOptionName = "1passwordmaccsv";
|
||||
private static Random _random = new();
|
||||
|
||||
private readonly MainPage _page;
|
||||
private readonly string _cacheDir;
|
||||
private readonly string _email;
|
||||
private readonly string _accountKey;
|
||||
private readonly string _password;
|
||||
private readonly string _domain;
|
||||
private readonly List<string> _excludedVaultNames;
|
||||
|
||||
public OnePasswordImportService(MainPage page, string cacheDir, string email, string accountKey,
|
||||
string password, string domain, string excludedVaultNames)
|
||||
{
|
||||
_page = page;
|
||||
_cacheDir = cacheDir;
|
||||
_email = email;
|
||||
_accountKey = accountKey;
|
||||
_password = password;
|
||||
_domain = domain;
|
||||
_excludedVaultNames = excludedVaultNames?
|
||||
.Split(",")
|
||||
.Select(n => n.Trim().ToLowerInvariant())
|
||||
.Where(n => !string.IsNullOrWhiteSpace(n))
|
||||
.ToList() ?? new List<string>();
|
||||
}
|
||||
|
||||
public async Task<(bool, string, string)> CreateImportFileAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var filteredAccounts = new List<Account>();
|
||||
|
||||
// Log in and get 1Password data
|
||||
var ui = new Ui(_page);
|
||||
var clientInfo = new ClientInfo
|
||||
{
|
||||
Username = _email,
|
||||
Password = _password,
|
||||
AccountKey = _accountKey,
|
||||
Uuid = RandomString(26).ToLowerInvariant(),
|
||||
Domain = string.IsNullOrWhiteSpace(_domain) ?
|
||||
PasswordManagerAccess.OnePassword.Region.Global.ToDomain() : _domain,
|
||||
DeviceName = "Importer",
|
||||
DeviceModel = "1.0.0",
|
||||
};
|
||||
|
||||
var session = Client.LogIn(clientInfo, ui, new MemoryStorage());
|
||||
try
|
||||
{
|
||||
var vaults = Client.ListAllVaults(session);
|
||||
foreach (var vaultInfo in vaults)
|
||||
{
|
||||
// Skip vaults we want to exclude
|
||||
if (string.IsNullOrWhiteSpace(vaultInfo.Name) ||
|
||||
_excludedVaultNames.Contains(vaultInfo.Name.ToLowerInvariant()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var vault = Client.OpenVault(vaultInfo, session);
|
||||
filteredAccounts.AddRange(vault.Accounts);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Client.LogOut(session);
|
||||
}
|
||||
|
||||
// Massage it to expected CSV format
|
||||
var exportAccounts = filteredAccounts.Select(a => new ExportedAccount(a));
|
||||
|
||||
// Create CSV string
|
||||
var csvOutput = CsvSerializer.SerializeToCsv(exportAccounts);
|
||||
|
||||
// Write CSV to temp disk
|
||||
var csvPath = Path.Combine(_cacheDir, "export.csv");
|
||||
await File.WriteAllTextAsync(csvPath, csvOutput);
|
||||
return (true, csvPath, ImportOptionName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (false, null, ImportOptionName);
|
||||
}
|
||||
}
|
||||
|
||||
private static string RandomString(int length)
|
||||
{
|
||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
return new string(Enumerable.Repeat(chars, length)
|
||||
.Select(s => s[_random.Next(s.Length)]).ToArray());
|
||||
}
|
||||
}
|
||||
19
src/Importer/Services/OnePassword/Ui.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using PasswordManagerAccess.OnePassword.Ui;
|
||||
|
||||
namespace Bit.Importer.Services.OnePassword;
|
||||
|
||||
public class Ui : BaseUi, IUi
|
||||
{
|
||||
public Ui(MainPage page)
|
||||
: base(page, "1Password") { }
|
||||
|
||||
public Passcode ProvideGoogleAuthPasscode()
|
||||
{
|
||||
return new Passcode(PromptCode("Enter your authenticator two-step login code."), false);
|
||||
}
|
||||
|
||||
public Passcode ProvideWebAuthnRememberMe()
|
||||
{
|
||||
return new Passcode(PromptCode("Enter your WebAuthn two-step login code."), false);
|
||||
}
|
||||
}
|
||||