Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db88da0c33 | ||
|
|
62c4afeccd | ||
|
|
92f2953e3f | ||
|
|
de5e2c6c20 | ||
|
|
12cedf6d67 | ||
|
|
244acf0222 | ||
|
|
1f04c0d388 | ||
|
|
7e4369622a | ||
|
|
d8ba8f9395 | ||
|
|
c22c79364d | ||
|
|
b6e021069f | ||
|
|
eb0181cba6 | ||
|
|
92c462a4d9 | ||
|
|
a25174bb13 | ||
|
|
94209bffcd |
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,10 @@
|
||||
> **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!
|
||||
|
||||

|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?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">
|
||||
@ -126,39 +126,99 @@
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<Label
|
||||
x:Name="ServiceLabel"
|
||||
Text="Import From Service" />
|
||||
<Picker
|
||||
x:Name="Service" />
|
||||
x:Name="Service"
|
||||
SelectedIndexChanged="Service_SelectedIndexChanged" />
|
||||
</VerticalStackLayout>
|
||||
|
||||
<VerticalStackLayout
|
||||
Spacing="5"
|
||||
Padding="0">
|
||||
<Label
|
||||
Text="LastPass Email" />
|
||||
<Entry
|
||||
x:Name="LastPassEmail" />
|
||||
</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
|
||||
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>
|
||||
<CheckBox
|
||||
x:Name="LastPassSkipShared"
|
||||
VerticalOptions="Center" />
|
||||
<Label
|
||||
x:Name="LastPassSkipSharedLabel"
|
||||
Text="Skip items from shared folders"
|
||||
VerticalOptions="Center" />
|
||||
</HorizontalStackLayout>
|
||||
<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
|
||||
@ -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,7 +12,7 @@ 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 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";
|
||||
@ -27,6 +28,7 @@ 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) =>
|
||||
@ -78,6 +80,12 @@ 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 (BitwardenApiKeyOption.IsChecked &&
|
||||
@ -118,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;
|
||||
}
|
||||
|
||||
@ -126,80 +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)
|
||||
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)
|
||||
{
|
||||
StopLoadingAndAlert(true,
|
||||
"Unable to log into your LastPass account. Are your credentials correct?");
|
||||
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 cliSetupSuccess = false;
|
||||
try
|
||||
var (loginSuccess, sessionKey) = LogInCli();
|
||||
if (!loginSuccess)
|
||||
{
|
||||
if (lastpassSuccess)
|
||||
if (BitwardenApiKeyOption.IsChecked)
|
||||
{
|
||||
await SetupCliAsync();
|
||||
cliSetupSuccess = true;
|
||||
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.");
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
||||
if (loginSuccess && string.IsNullOrWhiteSpace(sessionKey))
|
||||
{
|
||||
StopLoadingAndAlert(true, "Unable to set up Bitwarden CLI.");
|
||||
var (unlockSuccess, unlockSessionKey) = UnlockCli();
|
||||
sessionKey = unlockSessionKey;
|
||||
if (!unlockSuccess)
|
||||
{
|
||||
StopLoadingAndAlert(true,
|
||||
"Unable to unlock your Bitwarden vault. Is your master password correct?");
|
||||
}
|
||||
}
|
||||
|
||||
if (cliSetupSuccess && lastpassSuccess)
|
||||
if (!string.IsNullOrWhiteSpace(importFilePath) && !string.IsNullOrWhiteSpace(sessionKey))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(BitwardenServerUrl?.Text) && BitwardenServerUrl.Text != "https://bitwarden.com")
|
||||
var importSuccess = ImportCli(importOption, importFilePath, sessionKey);
|
||||
if (importSuccess)
|
||||
{
|
||||
var configServerSuccess = ConfigServerCli();
|
||||
if (!configServerSuccess)
|
||||
{
|
||||
StopLoadingAndAlert(true, "Unable to configure Bitwarden server.");
|
||||
return;
|
||||
}
|
||||
StopLoadingAndAlert(false, "Your import was successful!");
|
||||
ClearInputs();
|
||||
}
|
||||
|
||||
var (loginSuccess, sessionKey) = LogInCli();
|
||||
if (!loginSuccess)
|
||||
else
|
||||
{
|
||||
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(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.");
|
||||
}
|
||||
StopLoadingAndAlert(true, "Something went wrong with the import.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -228,39 +264,6 @@ 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, stdErr) = ExecCli($"config server {BitwardenServerUrl?.Text}");
|
||||
@ -442,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);
|
||||
}
|
||||
@ -488,6 +491,11 @@ public partial class MainPage : ContentPage
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
@ -572,6 +580,36 @@ public partial class MainPage : ContentPage
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -621,4 +659,4 @@ public partial class MainPage : ContentPage
|
||||
stream.Close();
|
||||
fileStream.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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);
|
||||
}
|
||||
}
|
||||