Compare commits
27 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 |
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
BUILD.md
@ -1,9 +1,11 @@
|
||||
# Build the macOS `.app`
|
||||
# Build the macOS `.app` and `.pkg`
|
||||
|
||||
1. Run release build of the `Importer.csproj`. Build will fail on the codesign verify step, however, the `Bitwarden Importer.app` will still be built and signed properly.
|
||||
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
|
||||
|
||||
@ -32,16 +34,9 @@
|
||||
|
||||
# Build the macOS `.pkg` artifact
|
||||
|
||||
1. Clean your bin folder and remove any files created from building and notarizing the `.app` previously.
|
||||
1. Follow steps for building the macOS `.pkg`.
|
||||
|
||||
2. Follow steps for building the macOS `.app`.
|
||||
|
||||
3. Create a `.pkg` by using the `productbuild` command.
|
||||
```
|
||||
productbuild --sign "Developer ID Installer: Bitwarden Inc (LTZ2PFU5D6)" --component "./Bitwarden Importer.app" /Applications "./Bitwarden Importer.pkg"
|
||||
```
|
||||
|
||||
4. Notarize the `.pkg` by following the steps for notarizing a macOS app.
|
||||
2. Notarize the `.pkg` by following the steps for notarizing a macOS app.
|
||||
|
||||
# Build the Windows `.msix` artifact
|
||||
|
||||
|
||||
@ -1,161 +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>
|
||||
|
||||
<HorizontalStackLayout>
|
||||
<CheckBox
|
||||
x:Name="LastPassSkipShared"
|
||||
VerticalOptions="Center" />
|
||||
<Label
|
||||
x:Name="LastPassSkipSharedLabel"
|
||||
Text="Skip items from shared folders"
|
||||
VerticalOptions="Center" />
|
||||
</HorizontalStackLayout>
|
||||
</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,70 +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 OobResult ApproveSalesforceAuth()
|
||||
{
|
||||
return OobResult.ContinueWithPasscode(PromptCode("Enter passcode from Salesforce Authenticator."), 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();
|
||||
}
|
||||
}
|
||||
11
README.md
@ -1,14 +1,20 @@
|
||||
> **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 credentails from Bitwarden and the old password management service and you're done!
|
||||
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`
|
||||
@ -16,6 +22,7 @@ You can use command line arguments to pre-populate any of the fields with defaul
|
||||
- `lastpassEmail=john.doe@company.com`
|
||||
- `lastpassMasterPassword=my-lastpass-master-password`
|
||||
- `lastpassSkipShared=1` (1 = checked)
|
||||
- `disableLastpassSkipShared=1` (1 = disabled)
|
||||
|
||||
## Special thanks
|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,22 @@ public partial class App : Application
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -20,8 +20,8 @@
|
||||
<ApplicationIdGuid>c5a31c67-9745-473c-b2ac-cd797bf4615b</ApplicationIdGuid>
|
||||
|
||||
<!-- Versions -->
|
||||
<ApplicationDisplayVersion>1.3</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>4</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>
|
||||
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>
|
||||
@ -1,5 +1,6 @@
|
||||
using PasswordManagerAccess.LastPass;
|
||||
using ServiceStack.Text;
|
||||
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;
|
||||
@ -11,9 +12,12 @@ 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" };
|
||||
private string _bitwardenCloudUrl = "https://bitwarden.com";
|
||||
private string _cliVersion = "2023.2.0";
|
||||
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()
|
||||
{
|
||||
@ -24,11 +28,12 @@ public partial class MainPage : ContentPage
|
||||
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"),
|
||||
await Browser.Default.OpenAsync(new Uri("https://bitwarden.com/help/bitwarden-importer-tool/"),
|
||||
BrowserLaunchMode.SystemPreferred);
|
||||
};
|
||||
LearnMore.GestureRecognizers.Add(learnMoreTap);
|
||||
@ -52,6 +57,12 @@ public partial class MainPage : ContentPage
|
||||
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
|
||||
@ -69,12 +80,32 @@ public partial class MainPage : ContentPage
|
||||
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 (string.IsNullOrWhiteSpace(BitwardenApiKeyClientId?.Text) ||
|
||||
string.IsNullOrWhiteSpace(BitwardenApiKeySecret?.Text))
|
||||
if (BitwardenApiKeyOption.IsChecked &&
|
||||
(string.IsNullOrWhiteSpace(BitwardenApiKeyClientId?.Text) ||
|
||||
string.IsNullOrWhiteSpace(BitwardenApiKeySecret?.Text)))
|
||||
{
|
||||
await DisplayAlert("Error", "Bitwarden API Key information is required.", "OK");
|
||||
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;
|
||||
}
|
||||
|
||||
@ -95,6 +126,18 @@ public partial class MainPage : ContentPage
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -103,71 +146,96 @@ public partial class MainPage : ContentPage
|
||||
await SetupAsync();
|
||||
await CleanupAsync();
|
||||
|
||||
if (_services[Service.SelectedIndex] == "LastPass")
|
||||
IImportService importService = null;
|
||||
var serviceSelection = _services[Service.SelectedIndex];
|
||||
if (serviceSelection == "LastPass")
|
||||
{
|
||||
var (lastpassSuccess, lastpassCsvPath) = await CreateLastpassCsvAsync();
|
||||
if (!lastpassSuccess)
|
||||
{
|
||||
StopLoadingAndAlert(true,
|
||||
"Unable to log into your LastPass account. Are your credentials correct?");
|
||||
}
|
||||
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 cliSetupSuccess = false;
|
||||
try
|
||||
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)
|
||||
{
|
||||
if (lastpassSuccess)
|
||||
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)
|
||||
{
|
||||
await SetupCliAsync();
|
||||
cliSetupSuccess = true;
|
||||
StopLoadingAndAlert(true, "Unable to configure Bitwarden server.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
StopLoadingAndAlert(true, "Unable to set up Bitwarden CLI.");
|
||||
}
|
||||
|
||||
if (cliSetupSuccess && lastpassSuccess)
|
||||
var (loginSuccess, sessionKey) = LogInCli();
|
||||
if (!loginSuccess)
|
||||
{
|
||||
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?");
|
||||
}
|
||||
|
||||
if (loginSuccess && string.IsNullOrWhiteSpace(sessionKey))
|
||||
else
|
||||
{
|
||||
var (unlockSuccess, unlockSessionKey) = UnlockCli();
|
||||
sessionKey = unlockSessionKey;
|
||||
if (!unlockSuccess)
|
||||
{
|
||||
StopLoadingAndAlert(true,
|
||||
"Unable to unlock your Bitwarden vault. Is your master password correct?");
|
||||
}
|
||||
StopLoadingAndAlert(true,
|
||||
"Unable to log into your Bitwarden account. " +
|
||||
"Try logging in using the API key option instead.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(lastpassCsvPath) && !string.IsNullOrWhiteSpace(sessionKey))
|
||||
if (loginSuccess && string.IsNullOrWhiteSpace(sessionKey))
|
||||
{
|
||||
var (unlockSuccess, unlockSessionKey) = UnlockCli();
|
||||
sessionKey = unlockSessionKey;
|
||||
if (!unlockSuccess)
|
||||
{
|
||||
var importSuccess = ImportCli("lastpasscsv", lastpassCsvPath, sessionKey);
|
||||
if (importSuccess)
|
||||
{
|
||||
StopLoadingAndAlert(false, "Your import was successful!");
|
||||
ClearInputs();
|
||||
}
|
||||
else
|
||||
{
|
||||
StopLoadingAndAlert(true, "Something went wrong with the import.");
|
||||
}
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -196,66 +264,83 @@ public partial class MainPage : ContentPage
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
new ParserOptions { ParseSecureNotesToAccount = false });
|
||||
|
||||
// Filter accounts
|
||||
var filteredAccounts = vault.Accounts.Where(a => !a.IsShared || (a.IsShared && !LastPassSkipShared.IsChecked));
|
||||
|
||||
// Massage it to expected CSV format
|
||||
var exportAccounts = filteredAccounts.Select(a => new Services.LastPass.ExportedAccount(a));
|
||||
|
||||
// Create CSV string
|
||||
var csvOutput = CsvSerializer.SerializeToCsv(exportAccounts);
|
||||
|
||||
// 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}");
|
||||
var (exitCode, stdOut, stdErr) = ExecCli($"config server {BitwardenServerUrl?.Text}");
|
||||
return exitCode == 0;
|
||||
}
|
||||
|
||||
private (bool, string) LogInCli()
|
||||
{
|
||||
var (exitCode, sessionKey) = ExecCli("login --apikey --raw", (process) =>
|
||||
var (exitCode, sessionKey, error) = (1, string.Empty, string.Empty);
|
||||
// API key login
|
||||
if (BitwardenApiKeyOption.IsChecked)
|
||||
{
|
||||
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";
|
||||
});
|
||||
(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) = ExecCli($"unlock {BitwardenPassword?.Text} --raw");
|
||||
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) = ExecCli($"import {importService} {importFilePath}", (process) =>
|
||||
var (importExitCode, importStdOut, stdErr) = ExecCli($"import {importService} {importFilePath}", (process) =>
|
||||
{
|
||||
process.StartInfo.EnvironmentVariables["BW_SESSION"] = sessionKey;
|
||||
});
|
||||
@ -275,14 +360,14 @@ public partial class MainPage : ContentPage
|
||||
|
||||
// Hash file
|
||||
var cliHashUrl = string.Format(
|
||||
"https://github.com/bitwarden/clients/releases/download/cli-v{0}/bw-{1}-sha256-{0}.txt",
|
||||
_cliVersion, isWindows ? "windows" : "macos");
|
||||
"{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("https://github.com/bitwarden/clients/releases/download/cli-v{0}/bw-{1}-{0}.zip",
|
||||
_cliVersion, isWindows ? "windows" : "macos");
|
||||
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);
|
||||
@ -314,7 +399,7 @@ public partial class MainPage : ContentPage
|
||||
}
|
||||
}
|
||||
|
||||
private (int, string) ExecCli(string args, Action<Process> processAction = null)
|
||||
private (int, string, string) ExecCli(string args, Action<Process> processAction = null)
|
||||
{
|
||||
// Set up the process
|
||||
using var process = new Process
|
||||
@ -325,6 +410,7 @@ public partial class MainPage : ContentPage
|
||||
Arguments = args,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
},
|
||||
};
|
||||
@ -336,13 +422,18 @@ public partial class MainPage : ContentPage
|
||||
|
||||
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());
|
||||
return (process.ExitCode, stdOut.Trim(), stdErr.Trim());
|
||||
}
|
||||
|
||||
private string ResolveCliPath()
|
||||
@ -354,7 +445,7 @@ public partial class MainPage : ContentPage
|
||||
private Task CleanupAsync()
|
||||
{
|
||||
File.Delete(Path.Combine(_cacheDir, "data.json"));
|
||||
File.Delete(Path.Combine(_cacheDir, "lastpass-export.csv"));
|
||||
File.Delete(Path.Combine(_cacheDir, "export.csv"));
|
||||
File.Delete(Path.Combine(_cacheDir, "bw.zip"));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
@ -392,12 +483,19 @@ public partial class MainPage : ContentPage
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
@ -423,6 +521,18 @@ public partial class MainPage : ContentPage
|
||||
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];
|
||||
@ -464,6 +574,42 @@ public partial class MainPage : ContentPage
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -491,8 +637,8 @@ public partial class MainPage : ContentPage
|
||||
|
||||
// Download hash from latest CLI release
|
||||
var cliHashUrl = string.Format(
|
||||
"https://github.com/bitwarden/clients/releases/download/cli-v{0}/bw-{1}-sha256-{0}.txt",
|
||||
_cliVersion, DeviceInfo.Platform == DevicePlatform.WinUI ? "windows" : "macos");
|
||||
"{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();
|
||||
@ -513,4 +659,4 @@ public partial class MainPage : ContentPage
|
||||
stream.Close();
|
||||
fileStream.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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.2.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();
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||