Compare commits

..

15 Commits

Author SHA1 Message Date
Daniel James Smith
db88da0c33
Merge pull request #21 from bitwarden/tools/pm-4565/archive-repo
[PM-4565] Add archive note to README.md
2023-12-04 17:05:54 +01:00
Daniel James Smith
62c4afeccd
Add archive note to README.md 2023-12-04 16:15:56 +01:00
Opeyemi
92f2953e3f
Merge pull request #17 from bitwarden/update-gh-actions-versions-to-main
All workflows - Update 'master' to 'main' for any actions in the gh-actions repository
2023-11-09 15:25:22 +00:00
Vince Grassia
de5e2c6c20
Update 'master' to 'main' 2023-11-08 13:52:15 -05:00
renovate[bot]
12cedf6d67
Update bitwarden/gh-actions digest to ba6a775 (#16)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-02 13:44:27 -04:00
renovate[bot]
244acf0222
Update bitwarden/gh-actions digest to c970b0f (#15)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-18 12:22:32 -04:00
renovate[bot]
1f04c0d388
Update bitwarden/gh-actions digest to f112580 (#14)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-05 14:56:44 -04:00
Kyle Spearrin
7e4369622a hide services selection when only 1 2023-09-29 09:43:23 -04:00
renovate[bot]
d8ba8f9395
Update bitwarden/gh-actions digest to 62d1bf7 (#11)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-25 08:36:54 -04:00
dwbit
c22c79364d
Correct typo (credentails to credentials) (#5) 2023-09-22 11:59:51 -04:00
Matt Bishop
b6e021069f
Repository housekeeping (#9)
* Repository housekeeping

* Tighten up ignores
2023-09-22 11:57:31 -04:00
Kyle Spearrin
eb0181cba6 finalize 1password import service 2023-06-22 12:45:03 -04:00
Kyle Spearrin
92c462a4d9 add UI for 1password import 2023-06-21 12:32:11 -04:00
Kyle Spearrin
a25174bb13 main page changes 2023-06-21 10:30:02 -04:00
Kyle Spearrin
94209bffcd refactor code for more importer services. add 1p 2023-06-21 10:29:52 -04:00
50 changed files with 896 additions and 200 deletions

138
.editorconfig Normal file
View 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
View File

@ -0,0 +1 @@
* text=auto eol=lf

11
.github/CODEOWNERS vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -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
View 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
View 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"
}
]
}

View File

@ -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();
}
}

View File

@ -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!
![Bitwarden Importer Screenshot](https://user-images.githubusercontent.com/1190944/236015514-76f2c282-73c3-442a-95a4-698c929e6ad5.png)

View File

@ -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
View File

@ -0,0 +1,6 @@
{
"sdk": {
"version": "7.0.401",
"rollForward": "latestFeature"
}
}

View File

@ -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

View File

@ -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();
}
}
}

View File

Before

Width:  |  Height:  |  Size: 228 B

After

Width:  |  Height:  |  Size: 228 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View 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();
}
}

View File

@ -0,0 +1,6 @@
namespace Bit.Importer.Services;
public interface IImportService
{
Task<(bool, string, string)> CreateImportFileAsync();
}

View 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);
}
}
}

View 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);
}
}

View 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;
}
}

View 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; }
}

View 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());
}
}

View 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);
}
}