Compare commits

...

55 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
Kyle Spearrin
c8ad33b69b hack to make window sizing work for macos 2023-05-16 14:58:32 -04:00
Kyle Spearrin
8a1d730d20
Update README.md 2023-05-16 14:28:22 -04:00
Kyle Spearrin
cd2ec5980a bump version 2023-05-16 14:24:28 -04:00
Kyle Spearrin
46b24b4a82 disable lastpass skip shared flag 2023-05-16 08:51:52 -04:00
Kyle Spearrin
9ccadeef1b
Update BUILD.md 2023-05-04 10:12:30 -04:00
Kyle Spearrin
3e1cdd3798 bump version for windows 2023-05-03 16:19:02 -04:00
Kyle Spearrin
17b5295684 remove unused exception variable 2023-05-03 16:06:47 -04:00
Kyle Spearrin
139d416a50 Merge branch 'master' of github.com:bitwarden/importer 2023-05-03 15:04:38 -04:00
Kyle Spearrin
83cdc517bb bump version 2023-05-03 15:04:09 -04:00
Kyle Spearrin
dccbf114ec
Update README.md 2023-05-03 14:56:31 -04:00
Kyle Spearrin
a757d46202 ref importer tool help article 2023-05-03 14:55:06 -04:00
Kyle Spearrin
b3f110e47f Add support for logging into Bitwarden with email/MP 2023-05-03 14:47:54 -04:00
Kyle Spearrin
29d3a4dcf0
Update BUILD.md 2023-03-28 11:11:33 -04:00
Kyle Spearrin
62330e0ed3 bump version 2023-03-28 10:41:13 -04:00
Kyle Spearrin
7b56da840e Add support for salesforce authenticator 2023-03-28 10:33:30 -04:00
Kyle Spearrin
54ee509a26 update libraries 2023-03-28 10:30:09 -04:00
Kyle Spearrin
aff5b74dc6 update build steps for macos 2023-03-07 16:47:35 -05:00
Kyle Spearrin
ed498678c3
bump version 2023-03-07 16:11:06 -05:00
Kyle Spearrin
e86f9dcfe4
Update BUILD.md 2023-03-07 16:06:23 -05:00
Kyle Spearrin
4ab67c34bc build instructions for macos 2023-03-07 16:05:09 -05:00
Kyle Spearrin
afa0e47495
build docs 2023-03-07 15:57:18 -05:00
Kyle Spearrin
6fe154e268
Merge branch 'master' of github.com:bitwarden/importer 2023-03-06 12:52:03 -05:00
Kyle Spearrin
5a1dbef16a
add checksum to CLI download 2023-03-06 12:51:56 -05:00
Kyle Spearrin
d1e8d76d01 codesigning for macos 2023-03-03 14:26:50 -05:00
Kyle Spearrin
6d0f5d9dcc
download CLI from internet 2023-03-03 13:42:22 -05:00
Kyle Spearrin
8d572bba88
update packages 2023-03-02 16:39:40 -05:00
Kyle Spearrin
2352b2f1ec
UseInterpreter to get macOS arm builds working 2023-03-01 14:35:07 -05:00
Kyle Spearrin
d05db2168f
add timestamp prefix to log messages 2023-02-28 12:09:34 -05:00
Kyle Spearrin
087ee24e1d
log helper 2023-02-28 12:08:02 -05:00
Kyle Spearrin
ddbe235e4f
switch from csvhelper to service stack 2023-02-27 20:35:46 -05:00
Kyle Spearrin
ef20df4776
Update README.md 2023-02-22 16:39:39 -05:00
Kyle Spearrin
af40f947b4
bump version 2023-02-22 16:23:17 -05:00
Kyle Spearrin
b3804be452
Update README.md 2023-02-22 16:20:42 -05:00
Kyle Spearrin
6c7e4269f7
Merge branch 'master' of github.com:bitwarden/importer 2023-02-22 16:18:48 -05:00
Kyle Spearrin
bc5b795a97
update libs. support favs, extras, and skipping shared 2023-02-22 16:18:42 -05:00
Kyle Spearrin
a9a1fad71f
Update README.md 2023-02-21 20:30:06 -05:00
Kyle Spearrin
3c9b08ca46
Update README.md 2023-02-21 20:28:52 -05:00
Kyle Spearrin
cc7d50d516
Update README.md 2023-02-21 20:26:32 -05:00
Kyle Spearrin
f0e14a8e08
Update README.md 2023-02-21 17:33:07 -05:00
Kyle Spearrin
a4c4284ad3
Update README.md 2023-02-21 17:31:05 -05:00
57 changed files with 2036 additions and 687 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"
}
]
}

51
BUILD.md Normal file
View File

@ -0,0 +1,51 @@
# Build the macOS `.app` and `.pkg`
1. Run release build of the `Importer.csproj`.
```
dotnet build -f:net7.0-maccatalyst -c:Release
```
Results (`.app` and `.pkg`) will be in the `./Importer/bin/Release/net7.0-maccatalyst/` directory.
# Notarize a macOS app
1. Use the included `notarizer.sh` script to notarize your application.
```
./notarizer.sh --notarize -a "./Importer/bin/Release/net7.0-maccatalyst/Bitwarden Importer.app" -b com.bitwarden.importer -u $APPLE_ID_USERNAME -p $APPLE_ID_PASSWORD -v LTZ2PFU5D6
```
2. Check the status of the notarization process with Apple by running the check command with `notarizer.sh`. The RequestUUID is available from the response in running the previous command.
```
./notarizer.sh --check -u $APPLE_ID_USERNAME -p $APPLE_ID_PASSWORD -k <REQUEST_UUID>
```
3. Once notarization is successful, staple the notarized application.
```
./notarizer.sh --staple --file "./Importer/bin/Release/net7.0-maccatalyst/Bitwarden Importer.app"
```
# Build the macOS `.zip` artifact
1. Follow steps for building the macOS `.app`.
2. Notarize the `Bitwarden Importer.app` by following the steps for notarizing a macOS app.
3. Zip up the `Bitwarden Importer.app` file for publishing.
# Build the macOS `.pkg` artifact
1. Follow steps for building the macOS `.pkg`.
2. Notarize the `.pkg` by following the steps for notarizing a macOS app.
# Build the Windows `.msix` artifact
1. Run release build of the `Importer.csproj`.
```
dotnet publish -f net7.0-windows10.0.19041.0 -c Release /p:RuntimeIdentifierOverride=win10-x64
```
2. Sign the created `.msix` with `azuresigntool`
```
azuresigntool.exe sign -v -kvu <URL> -kvi <ID> -kvt <TENANT> -kvs <SECRET> -kvc code-signing-certificate-3 -tr http://timestamp.digicert.com .\Importer_1.0.0.0_x64.msix
```

View File

@ -1,22 +0,0 @@
namespace Bit.Importer;
public partial class App : Application
{
public App()
{
// Uncomment below to force light theme
// Current.UserAppTheme = AppTheme.Light;
InitializeComponent();
MainPage = new AppShell();
}
protected override Window CreateWindow(IActivationState activationState)
{
var window = base.CreateWindow(activationState);
window.Width = 650;
window.Height = 1100;
window.Title = "Bitwarden Importer";
return window;
}
}

View File

@ -1,151 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.Importer.MainPage">
<ScrollView>
<VerticalStackLayout
Spacing="50"
Padding="20"
VerticalOptions="Start">
<VerticalStackLayout
Spacing="10"
Padding="0">
<Label
Text="The Bitwarden Importer Tool helps you easily move all of your data from an existing password management service into your Bitwarden account. Simply input your credentials for Bitwarden and your old password management service into the form below and click the 'Import' button." />
<Label
Text="Click here to learn more"
TextColor="{StaticResource Primary}"
FontAttributes="Bold"
TextDecorations="Underline"
x:Name="LearnMore" />
</VerticalStackLayout>
<VerticalStackLayout
Spacing="20"
Padding="0">
<VerticalStackLayout
Spacing="5"
Padding="0">
<Label
Text="Bitwarden Server Url" />
<Entry
x:Name="BitwardenServerUrl" />
</VerticalStackLayout>
<VerticalStackLayout
Spacing="5"
Padding="0">
<HorizontalStackLayout
Spacing="10">
<Label
Text="Bitwarden API Key client_id" />
<Label
Text="Click here to get your API key"
TextColor="{StaticResource Primary}"
FontAttributes="Bold"
TextDecorations="Underline"
x:Name="ApiKeyLink1" />
</HorizontalStackLayout>
<Entry
x:Name="BitwardenApiKeyClientId" />
</VerticalStackLayout>
<VerticalStackLayout
Spacing="5"
Padding="0">
<HorizontalStackLayout
Spacing="10">
<Label
Text="Bitwarden API Key secret" />
<Label
Text="Click here to get your API key"
TextColor="{StaticResource Primary}"
FontAttributes="Bold"
TextDecorations="Underline"
x:Name="ApiKeyLink2"/>
</HorizontalStackLayout>
<Entry
x:Name="BitwardenApiKeySecret"
IsPassword="True" />
</VerticalStackLayout>
<VerticalStackLayout
Spacing="5"
Padding="0"
x:Name="BitwardenPasswordLayout">
<Label
Text="Bitwarden Master Password" />
<Entry
x:Name="BitwardenPassword"
IsPassword="True" />
</VerticalStackLayout>
<HorizontalStackLayout
x:Name="BitwardenKeyConnectorLayout">
<CheckBox
x:Name="BitwardenKeyConnector"
CheckedChanged="BitwardenKeyConnector_CheckedChanged"
VerticalOptions="Center" />
<Label
x:Name="BitwardenKeyConnectorLabel"
Text="My organization uses a SSO configuration that does not require a master password."
VerticalOptions="Center" />
</HorizontalStackLayout>
</VerticalStackLayout>
<VerticalStackLayout
Spacing="20"
Padding="0">
<VerticalStackLayout
Spacing="5"
Padding="0">
<Label
Text="Import From Service" />
<Picker
x:Name="Service" />
</VerticalStackLayout>
<VerticalStackLayout
Spacing="5"
Padding="0">
<Label
Text="LastPass Email" />
<Entry
x:Name="LastPassEmail" />
</VerticalStackLayout>
<VerticalStackLayout
Spacing="5"
Padding="0">
<Label
Text="LastPass Master Password" />
<Entry
x:Name="LastPassPassword"
IsPassword="True" />
</VerticalStackLayout>
</VerticalStackLayout>
<HorizontalStackLayout
Spacing="10"
Padding="0">
<Button
x:Name="SubmitButton"
Text="Import"
Clicked="OnButtonClicked"
HorizontalOptions="Center" />
<ActivityIndicator
x:Name="Loading"
IsRunning="false"
VerticalOptions="Center" />
<Label
x:Name="PleaseWait"
Text="Please wait..."
IsVisible="false"
VerticalOptions="Center" />
</HorizontalStackLayout>
<Label
x:Name="CachePath"
IsVisible="false" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>

View File

@ -1,421 +0,0 @@
using CsvHelper;
using PasswordManagerAccess.LastPass;
using System.Diagnostics;
using System.Globalization;
namespace Bit.Importer;
public partial class MainPage : ContentPage
{
private readonly string _cacheDir;
private readonly List<string> _services = new() { "LastPass" };
private string _bitwardenCloudUrl = "https://bitwarden.com";
public MainPage()
{
_cacheDir = Path.Combine(FileSystem.CacheDirectory, "com.bitwarden.importer");
InitializeComponent();
BitwardenServerUrl.Text = _bitwardenCloudUrl;
Service.ItemsSource = _services;
Service.SelectedIndex = 0;
var learnMoreTap = new TapGestureRecognizer();
learnMoreTap.Tapped += async (s, e) =>
{
await Browser.Default.OpenAsync(new Uri("https://bitwarden.com/help"),
BrowserLaunchMode.SystemPreferred);
};
LearnMore.GestureRecognizers.Add(learnMoreTap);
var apiKeyTap = new TapGestureRecognizer();
apiKeyTap.Tapped += async (s, e) =>
{
await Browser.Default.OpenAsync(new Uri("https://vault.bitwarden.com/#/settings/security/security-keys"),
BrowserLaunchMode.SystemPreferred);
};
ApiKeyLink1.GestureRecognizers.Add(apiKeyTap);
ApiKeyLink2.GestureRecognizers.Add(apiKeyTap);
CachePath.Text = _cacheDir;
ParseCommandlineDefaults();
}
private void BitwardenKeyConnector_CheckedChanged(object sender, CheckedChangedEventArgs e)
{
// We don't need master passwqord if using key connector
BitwardenPasswordLayout.IsVisible = !BitwardenKeyConnector.IsChecked;
}
private async void OnButtonClicked(object sender, EventArgs e)
{
// Validate
if (!await ValidateInputsAsync())
{
return;
}
// Start loading state
Loading.IsRunning = true;
SubmitButton.IsEnabled = false;
PleaseWait.IsVisible = true;
// Run the import task
await Task.Run(ImportAsync);
}
private async Task<bool> ValidateInputsAsync()
{
if (string.IsNullOrWhiteSpace(BitwardenApiKeyClientId?.Text) ||
string.IsNullOrWhiteSpace(BitwardenApiKeySecret?.Text))
{
await DisplayAlert("Error", "Bitwarden API Key information is required.", "OK");
return false;
}
if (string.IsNullOrWhiteSpace(BitwardenPassword?.Text) &&
!BitwardenKeyConnector.IsChecked)
{
await DisplayAlert("Error", "Bitwarden master password is required.", "OK");
return false;
}
if (_services[Service.SelectedIndex] == "LastPass")
{
if (string.IsNullOrWhiteSpace(LastPassEmail?.Text) ||
string.IsNullOrWhiteSpace(LastPassPassword?.Text))
{
await DisplayAlert("Error", "LastPass Email and Master Password are required.", "OK");
return false;
}
}
return true;
}
private async Task ImportAsync()
{
await SetupAsync();
await CleanupAsync();
if (_services[Service.SelectedIndex] == "LastPass")
{
var (lastpassSuccess, lastpassCsvPath) = await CreateLastpassCsvAsync();
if (!lastpassSuccess)
{
StopLoadingAndAlert(true,
"Unable to log into your LastPass account. Are your credentials correct?");
}
var cliSetupSuccess = false;
try
{
if (lastpassSuccess)
{
await SetupCliAsync();
cliSetupSuccess = true;
}
}
catch
{
StopLoadingAndAlert(true, "Unable to set up Bitwarden CLI.");
}
if (cliSetupSuccess && lastpassSuccess)
{
if (!string.IsNullOrWhiteSpace(BitwardenServerUrl?.Text) && BitwardenServerUrl.Text != "https://bitwarden.com")
{
var configServerSuccess = ConfigServerCli();
if (!configServerSuccess)
{
StopLoadingAndAlert(true, "Unable to configure Bitwarden server.");
return;
}
}
var (loginSuccess, sessionKey) = LogInCli();
if (!loginSuccess)
{
StopLoadingAndAlert(true,
"Unable to log into your Bitwarden account. Is your API key information correct?");
}
if (loginSuccess && string.IsNullOrWhiteSpace(sessionKey))
{
var (unlockSuccess, unlockSessionKey) = UnlockCli();
sessionKey = unlockSessionKey;
if (!unlockSuccess)
{
StopLoadingAndAlert(true,
"Unable to unlock your Bitwarden vault. Is your master password correct?");
}
}
if (!string.IsNullOrWhiteSpace(lastpassCsvPath) && !string.IsNullOrWhiteSpace(sessionKey))
{
var importSuccess = ImportCli("lastpasscsv", lastpassCsvPath, sessionKey);
if (importSuccess)
{
StopLoadingAndAlert(false, "Your import was successful!");
ClearInputs();
}
else
{
StopLoadingAndAlert(true, "Something went wrong with the import.");
}
}
}
}
await CleanupAsync();
}
private void StopLoadingAndAlert(bool error, string message)
{
Dispatcher.Dispatch(async () =>
{
// Stop the loading state
Loading.IsRunning = false;
SubmitButton.IsEnabled = true;
PleaseWait.IsVisible = false;
// Show alert
if (error)
{
await DisplayAlert("Error", message, "OK");
}
else
{
await DisplayAlert("Success", message, "OK");
}
});
}
private async Task<(bool, string)> CreateLastpassCsvAsync()
{
try
{
// Log in and get LastPass data
var ui = new Services.LastPass.Ui(this);
var clientInfo = new ClientInfo(
PasswordManagerAccess.LastPass.Platform.Desktop,
Guid.NewGuid().ToString().ToLower(),
"Importer");
var vault = Vault.Open(LastPassEmail?.Text, LastPassPassword?.Text, clientInfo, ui);
// Massage it to expected CSV format
var exportAccounts = vault.Accounts.Select(a => new Services.LastPass.ExportedAccount(a));
// Create CSV string
using var writer = new StringWriter();
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
csv.WriteRecords(exportAccounts);
csv.Flush();
var csvOutput = writer.ToString();
// Write CSV to temp disk
var lastpassCsvPath = Path.Combine(_cacheDir, "lastpass-export.csv");
await File.WriteAllTextAsync(lastpassCsvPath, csvOutput);
return (true, lastpassCsvPath);
}
catch
{
return (false, null);
}
}
private bool ConfigServerCli()
{
var (exitCode, stdOut) = ExecCli($"config server {BitwardenServerUrl?.Text}");
return exitCode == 0;
}
private (bool, string) LogInCli()
{
var (exitCode, sessionKey) = ExecCli("login --apikey --raw", (process) =>
{
process.StartInfo.EnvironmentVariables["BW_CLIENTID"] = BitwardenApiKeyClientId?.Text;
process.StartInfo.EnvironmentVariables["BW_CLIENTSECRET"] = BitwardenApiKeySecret?.Text;
// Avoid BW_NOINTERACTION bug that is issuing invalid session key on api key login
process.StartInfo.EnvironmentVariables["BW_NOINTERACTION"] = "false";
});
return (exitCode == 0, sessionKey);
}
private (bool, string) UnlockCli()
{
var (exitCode, sessionKey) = ExecCli($"unlock {BitwardenPassword?.Text} --raw");
return (exitCode == 0, sessionKey);
}
private bool ImportCli(string importService, string importFilePath, string sessionKey)
{
var (importExitCode, importStdOut) = ExecCli($"import {importService} {importFilePath}", (process) =>
{
process.StartInfo.EnvironmentVariables["BW_SESSION"] = sessionKey;
});
return importExitCode == 0 && (importStdOut?.Contains("Imported") ?? false);
}
private async Task SetupCliAsync()
{
// Copy packaged CLI app to disk so that we can invoke it.
var isWindows = DeviceInfo.Platform == DevicePlatform.WinUI;
var cliFilename = isWindows ? "bw-windows.exe" : "bw-mac";
var cliPath = ResolveCliPath();
using var stream = await FileSystem.OpenAppPackageFileAsync($"bw-cli/{cliFilename}");
using var fileStream = File.Create(cliPath);
stream.Seek(0, SeekOrigin.Begin);
stream.CopyTo(fileStream);
stream.Close();
fileStream.Close();
if (!isWindows)
{
ExecBash($"chmod +x {cliPath}");
}
}
private (int, string) ExecCli(string args, Action<Process> processAction = null)
{
// Set up the process
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = ResolveCliPath(),
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
},
};
// Load standard env vars for this use case
process.StartInfo.EnvironmentVariables["BITWARDENCLI_APPDATA_DIR"] = _cacheDir;
process.StartInfo.EnvironmentVariables["BW_NOINTERACTION"] = "true";
processAction?.Invoke(process);
process.Start();
var stdOut = "";
while (!process.StandardOutput.EndOfStream)
{
stdOut += process.StandardOutput.ReadLine();
}
process.StandardOutput.Close();
process.WaitForExit();
return (process.ExitCode, stdOut.Trim());
}
private string ResolveCliPath()
{
var bwCliFilename = DeviceInfo.Platform == DevicePlatform.WinUI ? "bw.exe" : "bw";
return Path.Combine(_cacheDir, bwCliFilename);
}
private Task CleanupAsync()
{
File.Delete(Path.Combine(_cacheDir, "data.json"));
File.Delete(Path.Combine(_cacheDir, "lastpass-export.csv"));
return Task.FromResult(0);
}
private Task SetupAsync()
{
if (!Directory.Exists(_cacheDir))
{
Directory.CreateDirectory(_cacheDir);
}
return Task.FromResult(0);
}
public static void ExecBash(string cmd)
{
var escapedArgs = cmd.Replace("\"", "\\\"");
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
FileName = "/bin/bash",
Arguments = $"-c \"{escapedArgs}\""
}
};
process.Start();
process.WaitForExit();
}
private void ClearInputs()
{
BitwardenServerUrl.Text = string.Empty;
BitwardenApiKeyClientId.Text = string.Empty;
BitwardenApiKeySecret.Text = string.Empty;
BitwardenKeyConnector.IsChecked = false;
BitwardenPassword.Text = string.Empty;
LastPassEmail.Text = string.Empty;
LastPassPassword.Text = string.Empty;
}
private void ParseCommandlineDefaults()
{
var args = Environment.GetCommandLineArgs();
foreach (var arg in args)
{
if (!arg.Contains("="))
{
continue;
}
var argParts = arg.Split('=');
if (argParts.Length < 2)
{
continue;
}
if (argParts[0] == "bitwardenServerUrl")
{
BitwardenServerUrl.Text = argParts[1];
continue;
}
if (argParts[0] == "bitwardenApiKeyClientId")
{
BitwardenApiKeyClientId.Text = argParts[1];
continue;
}
if (argParts[0] == "bitwardenApiKeySecret")
{
BitwardenApiKeySecret.Text = argParts[1];
continue;
}
if (argParts[0] == "bitwardenMasterPassword")
{
BitwardenPassword.Text = argParts[1];
continue;
}
if (argParts[0] == "bitwardenKeyConnector")
{
BitwardenKeyConnector.IsChecked = argParts[1] == "1";
continue;
}
if (argParts[0] == "lastpassEmail")
{
LastPassEmail.Text = argParts[1];
continue;
}
if (argParts[0] == "lastpassMasterPassword")
{
LastPassPassword.Text = argParts[1];
continue;
}
}
}
}

Binary file not shown.

View File

@ -1,65 +0,0 @@
using PasswordManagerAccess.Common;
using PasswordManagerAccess.LastPass.Ui;
namespace Bit.Importer.Services.LastPass;
public class Ui : IUi
{
private readonly MainPage _page;
public Ui(MainPage page)
{
_page = page;
}
public OtpResult ProvideGoogleAuthPasscode()
{
return new OtpResult(PromptCode("Enter your authenticator two-step login code."), false);
}
public OtpResult ProvideMicrosoftAuthPasscode()
{
return new OtpResult(PromptCode("Enter your authenticator two-step login code."), false);
}
public OtpResult ProvideYubikeyPasscode()
{
return new OtpResult(PromptCode("Enter your Yubikey code."), false);
}
public OobResult ApproveLastPassAuth()
{
return OobResult.ContinueWithPasscode(PromptCode("Enter passcode from LastPass authenticator."), false);
}
public OobResult ApproveDuo()
{
return OobResult.ContinueWithPasscode(PromptCode("Enter passcode from Duo."), false);
}
public DuoChoice ChooseDuoFactor(DuoDevice[] devices)
{
var task = _page.Dispatcher.DispatchAsync(() =>
_page.DisplayActionSheet("Choose a Duo device", "Cancel", null, devices.Select(d => d.Name).ToArray()));
var actionSelection = task.GetAwaiter().GetResult();
var device = devices.FirstOrDefault(d => d.Name == actionSelection);
return new DuoChoice(device, DuoFactor.Passcode, false);
}
public string ProvideDuoPasscode(DuoDevice device)
{
return PromptCode("Enter your Duo passcode.");
}
public void UpdateDuoStatus(DuoStatus status, string text)
{
// Not sure what this is for.
}
private string PromptCode(string message)
{
var task = _page.Dispatcher.DispatchAsync(() =>
_page.DisplayPromptAsync("LastPass Two-step Login", message, "Submit"));
return task.GetAwaiter().GetResult();
}
}

View File

@ -1,2 +1,29 @@
# importer
Bitwarden standalone importer tool.
> **Archived**
>
> This repository is archived. Please go to https://bitwarden.com/help/import-from-lastpass/ for information on directly importing from LastPass.
# Bitwarden Importer
The Bitwarden Importer utility can be used to migrate individual vaults from another password management service, such as LastPass, without having to deal with the typical export process requiring CSV files. Just enter your credentials from Bitwarden and the old password management service and you're done!
![Bitwarden Importer Screenshot](https://user-images.githubusercontent.com/1190944/236015514-76f2c282-73c3-442a-95a4-698c929e6ad5.png)
## Command line args
You can use command line arguments to pre-populate any of the fields with default values:
- `bitwardenServerUrl=https://bitwarden.company.com`
- `bitwardenEmail=john.doe@company.com`
- `bitwardenApiKey=1` (1 = checked)
- `bitwardenApiKeyClientId=user.guid`
- `bitwardenApiKeySecret=myApiKeySecret`
- `bitwardenMasterPassword=my-bitwarden-master-password`
- `bitwardenKeyConnector=1` (1 = checked)
- `lastpassEmail=john.doe@company.com`
- `lastpassMasterPassword=my-lastpass-master-password`
- `lastpassSkipShared=1` (1 = checked)
- `disableLastpassSkipShared=1` (1 = disabled)
## Special thanks
A special thank you to the [Password Manager Access](https://github.com/detunized/password-manager-access) library that powers this application.

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"
}
}

286
notarizer.sh Executable file
View File

@ -0,0 +1,286 @@
#!/bin/bash
usage()
{
# Display Help
echo "*********************************************************************************************************************************************************"
echo " MacOS Application Notarization Script"
echo "*********************************************************************************************************************************************************"
echo
echo " Requirements"
echo " - XCode Installed"
echo " - Apple Id Account app-specific Password (https://support.apple.com/en-us/HT204397)"
echo " - Apple Developer ID Application Certificate created and installed in keychain (https://developer.apple.com/support/developer-id/)"
echo
echo " Instructions"
echo " 1. Run notarize option to code sign application and create notarization request"
echo " 2. Run check option with the request UUID of the previous step to check the notarization status"
echo " 3. Run staple option only if the notarization status was successful and package was approved"
echo " 4. You are now ready to distribute, if you want to create an installer you can use this option https://github.com/sindresorhus/create-dmg."
echo " Note that if you distribute your app in a .dmg, follow these steps:"
echo
echo " - Add your notarized and stapled app to the DMG."
echo " - Notarize your .dmg file."
echo " Example: sh $0 --notarize -a MyApp.dmg -b com.company.myapp -u myappleaccount@gmail.com -p aaaa-aaaa-aaaa-aaa -v FFFFFFFF)"
echo " - Staple the notarization to the .dmg file: xcrun stapler staple MyApp.dmg."
echo " Example: sh $0 --staple --file MyApp.dmg"
echo "________________________________________________________________________________________________________________________________________________________"
echo
echo " Usage"
echo " $0 [-n|s|c] [ -a APP_NAME ] [ -i SIGNING_IDENTITY ] [ -e ENTITLEMENTS ] [ -b BUNDLE_ID ] [ -u USERNAME ] [ -p PASSWORD ] [ -v PROVIDER ] [ -k UUID ]"
echo
echo "________________________________________________________________________________________________________________________________________________________"
echo
echo " Options:"
echo
notarizeHelp
checkHelp
stapleHelp
return
}
notarizeHelp()
{
echo " ======================================================================="
echo " -n | --notarize Notarize file"
echo " ======================================================================="
echo " Syntax:"
echo " [ -n | --notarize ] [ -a | --file APP_NAME ] [ -i SIGNING_IDENTITY ] [ -e ENTITLEMENTS ] [ -b BUNDLE_ID ] [ -u USERNAME ] [ -p PASSWORD ] [ -v PROVIDER ]"
echo " Parameters:"
echo " [ -a | --file ] - File name"
echo " [ -i ] - Apple Signing identity"
echo " [ -e ] - Application entitlements file"
echo " [ -b ] - Application Bundle identifier"
echo " [ -u ] - Apple Developer ID Username"
echo " [ -p ] - Application Specific password"
echo " [ -v ] - Access Provider"
echo " Example:"
echo " .app sh $0 --notarize -a MyApp.app -b com.company.myapp -u myappleaccount@gmail.com -p aaaa-aaaa-aaaa-aaa -v FFFFFFFF -e App.entitlements -i \"Developer ID Application: COMPANY\""
echo " .zip sh $0 --notarize -a MyApp.app.zip -b com.company.myapp -u myappleaccount@gmail.com -p aaaa-aaaa-aaaa-aaa -v FFFFFFFF"
echo " .dmg sh $0 --notarize -a MyApp.dmg -b com.company.myapp -u myappleaccount@gmail.com -p aaaa-aaaa-aaaa-aaa -v FFFFFFFF"
echo
}
checkHelp()
{
echo " ======================================================================="
echo " -c | --check Check notarization status"
echo " ======================================================================="
echo " Syntax:"
echo " [ -c | --check ] [ -u USERNAME ] [ -p PASSWORD ] [ -k UUID ]"
echo " Parameters:"
echo " [ -u ] - Apple Developer ID Username"
echo " [ -p ] - Application Specific password"
echo " [ -k ] - Notarization Request UUID"
echo " Example:"
echo " sh $0 --check -u myappleaccount@gmail.com -p aaaa-aaaa-aaaa-aaa -k ffff-ffffff-ffffff-ffffffffff"
echo
}
stapleHelp()
{
echo " ======================================================================="
echo " -s | --staple Staple file"
echo " ======================================================================="
echo " Syntax:"
echo " [ -s | --staple ] [ -a | --file APP_NAME ]"
echo " Parameters:"
echo " [ -a | --file ] - File name"
echo " Example:"
echo " sh $0 --staple --file MyApp.app"
echo
}
#Help Dictionary
helpFunction()
{
echo ""
usage
exit 1
}
# Transform long options to short ones
for arg in "$@"; do
shift
case "$arg" in
"--notarize") set -- "$@" "-n" ;;
"--staple") set -- "$@" "-s" ;;
"--check") set -- "$@" "-c" ;;
"--file") set -- "$@" "-a" ;;
*) set -- "$@" "$arg"
esac
done
while getopts "nsca:i:e:b:v:u:k:p:" option
do
case "${option}"
in
n) ACTION=NOTARIZE;;
s) ACTION=STAPLE;;
c) ACTION=CHECK;;
a) APP_NAME=${OPTARG};;
i) SIGNING_IDENTITY=${OPTARG};;
e) ENTITLEMENTS=${OPTARG};;
b) BUNDLE_ID=${OPTARG};;
p) PASSWORD=${OPTARG};;
v) PROVIDER=${OPTARG};;
u) USERNAME=${OPTARG};;
k) UUID=${OPTARG};;
?) helpFunction ;;
esac
done
do_check()
{
echo "$UUID"
if [ -z "${UUID}" ]; then
echo "[Error] Didn't specify notarization request UUID";
fi
if [ -z "${USERNAME}" ]; then
echo "[Error] Apple ID username is required";
fi
if [ -z "${PASSWORD}" ]; then
echo "[Error] App Specific password is required";
fi
if [ -z "${UUID}" ] || [ -z "${USERNAME}" ] || [ -z "${PASSWORD}" ]; then
echo
checkHelp
exit 1
fi
echo "[INFO] Checking Notarization status for $UUID"
xcrun altool --notarization-info "$UUID" -u "$USERNAME" -p "$PASSWORD" --output-format xml
exit 1
}
sign()
{
if [ -z "${APP_NAME}" ]; then
echo "[Error] Didn't specify a filename";
fi
if [ -z "${BUNDLE_ID}" ]; then
echo "[Error] Didn't specify bundle identifier";
fi
if [ -z "${PROVIDER}" ]; then
echo "[Error] Didn't specify access provider";
fi
if [ -z "${USERNAME}" ]; then
echo "[Error] Apple ID username is required";
fi
if [ -z "${PASSWORD}" ]; then
echo "[Error] App Specific password is required";
fi
if [ -z "${APP_NAME}" ] || [ -z "${USERNAME}" ] || [ -z "${PASSWORD}" ] || [ -z "${PROVIDER}" ]; then
echo
notarizeHelp
exit 1
fi
#echo "[INFO] Signing app contents"
#find "$APP_NAME/Contents"|while read fname; do
# if [[ -f $fname ]]; then
# echo "[INFO] Signing $fname"
# codesign --force --timestamp --options=runtime --entitlements "$ENTITLEMENTS" --sign "$SIGNING_IDENTITY" $fname
# fi
#done
#echo "[INFO] Signing app file"
#codesign --force --timestamp --options=runtime --entitlements "$ENTITLEMENTS" --sign "$SIGNING_IDENTITY" "$APP_NAME"
echo "[INFO] Verifying Code Sign"
codesign --verify --verbose "$APP_NAME"
echo "[INFO] Zipping $APP_NAME to ${APP_NAME}.zip"
ditto -c -k --rsrc --keepParent "$APP_NAME" "${APP_NAME}.zip"
#echo "[INFO] Uploading $APP_NAME for notarization"
#xcrun altool --notarize-app -t osx -f "${APP_NAME}.zip" --primary-bundle-id "$BUNDLE_ID" -u "$USERNAME" -p "$PASSWORD" --asc-provider "$PROVIDER" --output-format xml
notarizationUpload "${APP_NAME}.zip"
}
notarize()
{
if [ -z "${APP_NAME}" ]; then
echo "[Error] Didn't specify a filename";
fi
if [ -z "${BUNDLE_ID}" ]; then
echo "[Error] Didn't specify bundle identifier";
fi
if [ -z "${PROVIDER}" ]; then
echo "[Error] Didn't specify access provider";
fi
if [ -z "${USERNAME}" ]; then
echo "[Error] Apple ID username is required";
fi
if [ -z "${PASSWORD}" ]; then
echo "[Error] App Specific password is required";
fi
if [ -z "${APP_NAME}" ] || [ -z "${USERNAME}" ] || [ -z "${BUNDLE_ID}" ] || [ -z "${PASSWORD}" ] || [ -z "${PROVIDER}" ]; then
echo
notarizeHelp
exit 1
fi
case "$APP_NAME" in
*.app) sign;;
*.zip) notarizationUpload "$APP_NAME";;
*.dmg) notarizationUpload "$APP_NAME";;
*.pkg) notarizationUpload "$APP_NAME";;
esac
}
notarizationUpload()
{
echo "[INFO] Uploading $APP_NAME for notarization"
xcrun altool --notarize-app -t osx -f "$1" --primary-bundle-id "$BUNDLE_ID" -u "$USERNAME" -p "$PASSWORD" --asc-provider "$PROVIDER" --output-format xml
}
do_staple()
{
if [ -z "${APP_NAME}" ]; then
echo "[Error] Didn't specify a filename";
echo
stapleHelp
exit 1
fi
echo "[INFO] Stapling $APP_NAME"
xcrun stapler staple "$APP_NAME"
echo "[INFO] Validating Staple for $APP_NAME"
xcrun stapler validate "$APP_NAME"
}
#Excute Action base on the option -s -n -c
case $ACTION in
STAPLE) do_staple;;
CHECK) do_check;;
NOTARIZE) notarize;;
*) helpFunction;
esac
unset APP_NAME ACTION SIGNING_IDENTITY BUNDLE_ID ENTITLEMENTS USERNAME PASSWORD PROVIDER UUID

38
src/Importer/App.xaml.cs Normal file
View File

@ -0,0 +1,38 @@
namespace Bit.Importer;
public partial class App : Application
{
public App()
{
// Uncomment below to force light theme
// Current.UserAppTheme = AppTheme.Light;
InitializeComponent();
MainPage = new AppShell();
}
protected override Window CreateWindow(IActivationState activationState)
{
var window = base.CreateWindow(activationState);
window.Width = 650;
window.Height = 1150;
window.Title = "Bitwarden Importer";
if (DeviceInfo.Platform != DevicePlatform.WinUI)
{
window.MinimumWidth = 650;
window.MinimumHeight = 1150;
Dispatcher.DispatchAsync(async () =>
{
await Task.Delay(1000);
window.MinimumWidth = 0;
window.MinimumHeight = 0;
window.MaximumWidth = double.PositiveInfinity;
window.MaximumHeight = double.PositiveInfinity;
});
}
return window;
}
}

View File

@ -8,6 +8,7 @@
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<UseInterpreter>true</UseInterpreter>
<DefaultLanguage>en</DefaultLanguage>
<AssemblyName>Bitwarden Importer</AssemblyName>
@ -19,8 +20,8 @@
<ApplicationIdGuid>c5a31c67-9745-473c-b2ac-cd797bf4615b</ApplicationIdGuid>
<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.4</ApplicationDisplayVersion>
<ApplicationVersion>5</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">13.1</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
@ -32,6 +33,25 @@
<WindowsAppSdkDeploymentManagerInitialize>false</WindowsAppSdkDeploymentManagerInitialize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net7.0-maccatalyst|AnyCPU'">
<CodesignProvision>Bitwarden Importer Developer App No Cat</CodesignProvision>
<CodesignKey>Developer ID Application: Bitwarden Inc (LTZ2PFU5D6)</CodesignKey>
<PackageSigningKey>Developer ID Installer: Bitwarden Inc (LTZ2PFU5D6)</PackageSigningKey>
<ProvisionType>Manual</ProvisionType>
<EnableCodeSigning>true</EnableCodeSigning>
<CodesignEntitlements>Platforms\MacCatalyst\Entitlements.plist</CodesignEntitlements>
<UseHardenedRuntime>true</UseHardenedRuntime>
<CreatePackage>true</CreatePackage>
<EnablePackageSigning>true</EnablePackageSigning>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net7.0-maccatalyst|AnyCPU'">
<CreatePackage>false</CreatePackage>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#175DDC" />
@ -51,14 +71,12 @@
<ItemGroup>
<None Remove="Resources\Images\icon.svg" />
<None Remove="Resources\Raw\bw-cli\bw-mac" />
<None Remove="Resources\Raw\bw-cli\bw-windows.exe" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
<PackageReference Include="PasswordManagerAccess" Version="10.2.1" />
<PackageReference Include="PasswordManagerAccess" Version="12.0.0" />
<PackageReference Include="ServiceStack.Text" Version="6.7.0" />
</ItemGroup>
</Project>

249
src/Importer/MainPage.xaml Normal file
View 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>

View File

@ -0,0 +1,662 @@
using Bit.Importer.Services;
using Bit.Importer.Services.LastPass;
using Bit.Importer.Services.OnePassword;
using System.Diagnostics;
using System.IO.Compression;
using System.Security.Cryptography;
namespace Bit.Importer;
public partial class MainPage : ContentPage
{
private readonly HttpClient _httpClient = new();
private readonly bool _doLogging = false;
private readonly string _cacheDir;
private readonly List<string> _services = new() { "LastPass"/*, "1Password"*/ };
private readonly string _cliVersion = "2023.4.0";
private readonly string _cliBaseDownloadUrl = "https://assets.bitwarden.com/importer";
private readonly string _bitwardenCloudUrl = "https://bitwarden.com";
private readonly Dictionary<string, int> _twoFactorMethods =
new() { { "Authenticator app", 0 }, { "Email", 1 }, { "YubiKey OTP security key", 3 } };
public MainPage()
{
_cacheDir = Path.Combine(FileSystem.CacheDirectory, "com.bitwarden.importer");
InitializeComponent();
BitwardenServerUrl.Text = _bitwardenCloudUrl;
Service.ItemsSource = _services;
Service.SelectedIndex = 0;
Service.IsVisible = ServiceLabel.IsVisible = _services.Count > 1;
var learnMoreTap = new TapGestureRecognizer();
learnMoreTap.Tapped += async (s, e) =>
{
await Browser.Default.OpenAsync(new Uri("https://bitwarden.com/help/bitwarden-importer-tool/"),
BrowserLaunchMode.SystemPreferred);
};
LearnMore.GestureRecognizers.Add(learnMoreTap);
var apiKeyTap = new TapGestureRecognizer();
apiKeyTap.Tapped += async (s, e) =>
{
await Browser.Default.OpenAsync(new Uri("https://vault.bitwarden.com/#/settings/security/security-keys"),
BrowserLaunchMode.SystemPreferred);
};
ApiKeyLink1.GestureRecognizers.Add(apiKeyTap);
ApiKeyLink2.GestureRecognizers.Add(apiKeyTap);
CachePath.Text = _cacheDir;
ParseCommandlineDefaults();
}
private void BitwardenKeyConnector_CheckedChanged(object sender, CheckedChangedEventArgs e)
{
// We don't need master passwqord if using key connector
BitwardenPasswordLayout.IsVisible = !BitwardenKeyConnector.IsChecked;
}
private void BitwardenApiKeyOption_CheckedChanged(object sender, CheckedChangedEventArgs e)
{
BitwardenApiKeyLayout.IsVisible = BitwardenApiKeyOption.IsChecked;
BitwardenEmailLayout.IsVisible = !BitwardenApiKeyOption.IsChecked;
}
private async void OnButtonClicked(object sender, EventArgs e)
{
// Validate
if (!await ValidateInputsAsync())
{
return;
}
// Start loading state
Loading.IsRunning = true;
SubmitButton.IsEnabled = false;
PleaseWait.IsVisible = true;
// Run the import task
await Task.Run(ImportAsync);
}
private void Service_SelectedIndexChanged(object sender, EventArgs e)
{
LastPassLayout.IsVisible = _services[Service.SelectedIndex] == "LastPass";
OnePasswordLayout.IsVisible = _services[Service.SelectedIndex] == "1Password";
}
private async Task<bool> ValidateInputsAsync()
{
if (BitwardenApiKeyOption.IsChecked &&
(string.IsNullOrWhiteSpace(BitwardenApiKeyClientId?.Text) ||
string.IsNullOrWhiteSpace(BitwardenApiKeySecret?.Text)))
{
await DisplayAlert("Error", "Bitwarden API key information is required.", "OK");
return false;
}
if (!BitwardenApiKeyOption.IsChecked &&
string.IsNullOrWhiteSpace(BitwardenEmail?.Text))
{
await DisplayAlert("Error", "Bitwarden email is required.", "OK");
return false;
}
if (BitwardenKeyConnector.IsChecked && !BitwardenApiKeyOption.IsChecked)
{
await DisplayAlert("Error", "Bitwarden APIs keys are required when your organization uses SSO.", "OK");
return false;
}
if (string.IsNullOrWhiteSpace(BitwardenPassword?.Text) &&
!BitwardenKeyConnector.IsChecked)
{
await DisplayAlert("Error", "Bitwarden master password is required.", "OK");
return false;
}
if (_services[Service.SelectedIndex] == "LastPass")
{
if (string.IsNullOrWhiteSpace(LastPassEmail?.Text) ||
string.IsNullOrWhiteSpace(LastPassPassword?.Text))
{
await DisplayAlert("Error", "LastPass Email and Master Password are required.", "OK");
return false;
}
}
if (_services[Service.SelectedIndex] == "1Password")
{
if (string.IsNullOrWhiteSpace(OnePasswordEmail?.Text) ||
string.IsNullOrWhiteSpace(OnePasswordSecretKey?.Text) ||
string.IsNullOrWhiteSpace(OnePasswordDomain?.Text) ||
string.IsNullOrWhiteSpace(OnePasswordPassword?.Text))
{
await DisplayAlert("Error", "1Password information is required.", "OK");
return false;
}
}
return true;
}
private async Task ImportAsync()
{
await SetupAsync();
await CleanupAsync();
IImportService importService = null;
var serviceSelection = _services[Service.SelectedIndex];
if (serviceSelection == "LastPass")
{
importService = new LastPassImportService(this, _cacheDir,
LastPassEmail?.Text, LastPassPassword?.Text, LastPassSkipShared.IsChecked);
}
else if (serviceSelection == "1Password")
{
importService = new OnePasswordImportService(this, _cacheDir,
OnePasswordEmail?.Text, OnePasswordSecretKey?.Text, OnePasswordPassword?.Text,
OnePasswordDomain?.Text, OnePasswordExcludedVaults.Text);
}
var (serviceSuccess, importFilePath, importOption) = (false, string.Empty, string.Empty);
if (importService != null)
{
(serviceSuccess, importFilePath, importOption) = await importService.CreateImportFileAsync();
}
if (!serviceSuccess)
{
StopLoadingAndAlert(true,
$"Unable to log into your {serviceSelection} account. Are your credentials correct?");
}
var cliSetupSuccess = false;
try
{
if (serviceSuccess)
{
await SetupCliAsync();
cliSetupSuccess = true;
}
}
catch
{
StopLoadingAndAlert(true, "Unable to set up Bitwarden CLI.");
}
if (cliSetupSuccess && serviceSuccess)
{
if (!string.IsNullOrWhiteSpace(BitwardenServerUrl?.Text) && BitwardenServerUrl.Text != "https://bitwarden.com")
{
var configServerSuccess = ConfigServerCli();
if (!configServerSuccess)
{
StopLoadingAndAlert(true, "Unable to configure Bitwarden server.");
return;
}
}
var (loginSuccess, sessionKey) = LogInCli();
if (!loginSuccess)
{
if (BitwardenApiKeyOption.IsChecked)
{
StopLoadingAndAlert(true,
"Unable to log into your Bitwarden account. Is your API key information correct?");
}
else
{
StopLoadingAndAlert(true,
"Unable to log into your Bitwarden account. " +
"Try logging in using the API key option instead.");
}
}
if (loginSuccess && string.IsNullOrWhiteSpace(sessionKey))
{
var (unlockSuccess, unlockSessionKey) = UnlockCli();
sessionKey = unlockSessionKey;
if (!unlockSuccess)
{
StopLoadingAndAlert(true,
"Unable to unlock your Bitwarden vault. Is your master password correct?");
}
}
if (!string.IsNullOrWhiteSpace(importFilePath) && !string.IsNullOrWhiteSpace(sessionKey))
{
var importSuccess = ImportCli(importOption, importFilePath, sessionKey);
if (importSuccess)
{
StopLoadingAndAlert(false, "Your import was successful!");
ClearInputs();
}
else
{
StopLoadingAndAlert(true, "Something went wrong with the import.");
}
}
}
await CleanupAsync();
}
private void StopLoadingAndAlert(bool error, string message)
{
Dispatcher.Dispatch(async () =>
{
// Stop the loading state
Loading.IsRunning = false;
SubmitButton.IsEnabled = true;
PleaseWait.IsVisible = false;
// Show alert
if (error)
{
await DisplayAlert("Error", message, "OK");
}
else
{
await DisplayAlert("Success", message, "OK");
}
});
}
private bool ConfigServerCli()
{
var (exitCode, stdOut, stdErr) = ExecCli($"config server {BitwardenServerUrl?.Text}");
return exitCode == 0;
}
private (bool, string) LogInCli()
{
var (exitCode, sessionKey, error) = (1, string.Empty, string.Empty);
// API key login
if (BitwardenApiKeyOption.IsChecked)
{
(exitCode, sessionKey, error) = ExecCli("login --apikey --raw", (process) =>
{
process.StartInfo.EnvironmentVariables["BW_CLIENTID"] = BitwardenApiKeyClientId?.Text;
process.StartInfo.EnvironmentVariables["BW_CLIENTSECRET"] = BitwardenApiKeySecret?.Text;
// Avoid BW_NOINTERACTION bug that is issuing invalid session key on api key login
process.StartInfo.EnvironmentVariables["BW_NOINTERACTION"] = "false";
});
}
// Master password login
else
{
var command = string.Format("login {0} {1} --raw", BitwardenEmail?.Text, BitwardenPassword?.Text);
(exitCode, sessionKey, error) = ExecCli(command);
if (exitCode == 1)
{
var promptMethod = error.Contains("no provider selected", StringComparison.CurrentCultureIgnoreCase);
var promptCode = promptMethod || error.Contains("code is required", StringComparison.CurrentCultureIgnoreCase);
int? method = null;
string code = null;
if (promptMethod)
{
var methodTask = Dispatcher.DispatchAsync(() =>
DisplayActionSheet("Select Bitwarden 2FA method.", "Cancel", null,
_twoFactorMethods.Keys.ToArray()));
var methodKey = methodTask.GetAwaiter().GetResult();
if (!string.IsNullOrWhiteSpace(methodKey))
{
method = _twoFactorMethods[methodKey];
}
}
if (promptCode)
{
var codeTask = Dispatcher.DispatchAsync(() =>
DisplayPromptAsync("Bitwarden Two-step Login", "Enter your two-step login code.", "Submit"));
code = codeTask.GetAwaiter().GetResult();
}
if (method.HasValue)
{
command = string.Format("login {0} {1} --method {2} --code {3} --raw",
BitwardenEmail?.Text, BitwardenPassword?.Text, method.Value, code);
}
else
{
command = string.Format("login {0} {1} --code {2} --raw",
BitwardenEmail?.Text, BitwardenPassword?.Text, code);
}
(exitCode, sessionKey, error) = ExecCli(command);
}
}
return (exitCode == 0, sessionKey);
}
private (bool, string) UnlockCli()
{
var (exitCode, sessionKey, error) = ExecCli($"unlock {BitwardenPassword?.Text} --raw");
return (exitCode == 0, sessionKey);
}
private bool ImportCli(string importService, string importFilePath, string sessionKey)
{
var (importExitCode, importStdOut, stdErr) = ExecCli($"import {importService} {importFilePath}", (process) =>
{
process.StartInfo.EnvironmentVariables["BW_SESSION"] = sessionKey;
});
return importExitCode == 0 && (importStdOut?.Contains("Imported") ?? false);
}
private async Task SetupCliAsync()
{
if (await HasLatestCliAsync())
{
return;
}
// Download CLI app to disk so that we can invoke it.
var isWindows = DeviceInfo.Platform == DevicePlatform.WinUI;
// Hash file
var cliHashUrl = string.Format(
"{0}/cli-v{1}/bw-{2}-sha256-{1}.txt",
_cliBaseDownloadUrl, _cliVersion, isWindows ? "windows" : "macos");
var cliHashFilename = Path.Combine(_cacheDir, "bw.sha256.txt");
await DownloadFileAsync(cliHashUrl, cliHashFilename);
// Zip file
var cliUrl = string.Format("{0}/cli-v{1}/bw-{2}-{1}.zip",
_cliBaseDownloadUrl, _cliVersion, isWindows ? "windows" : "macos");
var cliZipFilename = Path.Combine(_cacheDir, "bw.zip");
var cliPath = ResolveCliPath();
await DownloadFileAsync(cliUrl, cliZipFilename);
// Verify checksums
using var hashFileStream = File.OpenRead(cliHashFilename);
using var hashFileReader = new StreamReader(hashFileStream);
var hashFileHex = hashFileReader.ReadToEnd().Trim();
hashFileStream.Close();
using var zipStream = File.OpenRead(cliZipFilename);
using var sha256 = SHA256.Create();
var zipHashBytes = sha256.ComputeHash(zipStream);
var zipHashHex = BitConverter.ToString(zipHashBytes).Replace("-", string.Empty);
zipStream.Close();
if (!string.Equals(zipHashHex, hashFileHex, StringComparison.InvariantCultureIgnoreCase))
{
throw new Exception("CLI checksum failed.");
}
// Extract zip
ZipFile.ExtractToDirectory(cliZipFilename, _cacheDir, true);
// macOS permissions
if (!isWindows)
{
ExecBash($"chmod +x {cliPath}");
}
}
private (int, string, string) ExecCli(string args, Action<Process> processAction = null)
{
// Set up the process
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = ResolveCliPath(),
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
},
};
// Load standard env vars for this use case
process.StartInfo.EnvironmentVariables["BITWARDENCLI_APPDATA_DIR"] = _cacheDir;
process.StartInfo.EnvironmentVariables["BW_NOINTERACTION"] = "true";
processAction?.Invoke(process);
process.Start();
var stdOut = "";
var stdErr = "";
while (!process.StandardOutput.EndOfStream)
{
stdOut += process.StandardOutput.ReadLine();
}
while (!process.StandardError.EndOfStream)
{
stdErr += process.StandardError.ReadLine();
}
process.StandardOutput.Close();
process.WaitForExit();
return (process.ExitCode, stdOut.Trim(), stdErr.Trim());
}
private string ResolveCliPath()
{
var bwCliFilename = DeviceInfo.Platform == DevicePlatform.WinUI ? "bw.exe" : "bw";
return Path.Combine(_cacheDir, bwCliFilename);
}
private Task CleanupAsync()
{
File.Delete(Path.Combine(_cacheDir, "data.json"));
File.Delete(Path.Combine(_cacheDir, "export.csv"));
File.Delete(Path.Combine(_cacheDir, "bw.zip"));
return Task.FromResult(0);
}
private Task SetupAsync()
{
if (!Directory.Exists(_cacheDir))
{
Directory.CreateDirectory(_cacheDir);
}
return Task.FromResult(0);
}
public static void ExecBash(string cmd)
{
var escapedArgs = cmd.Replace("\"", "\\\"");
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
FileName = "/bin/bash",
Arguments = $"-c \"{escapedArgs}\""
}
};
process.Start();
process.WaitForExit();
}
private void ClearInputs()
{
Dispatcher.Dispatch(() =>
{
BitwardenServerUrl.Text = string.Empty;
BitwardenEmail.Text = string.Empty;
BitwardenApiKeyOption.IsChecked = false;
BitwardenApiKeyClientId.Text = string.Empty;
BitwardenApiKeySecret.Text = string.Empty;
BitwardenKeyConnector.IsChecked = false;
BitwardenPassword.Text = string.Empty;
LastPassEmail.Text = string.Empty;
LastPassPassword.Text = string.Empty;
OnePasswordEmail.Text = string.Empty;
OnePasswordPassword.Text = string.Empty;
OnePasswordSecretKey.Text = string.Empty;
OnePasswordDomain.Text = string.Empty;
OnePasswordExcludedVaults.Text = string.Empty;
});
}
private void ParseCommandlineDefaults()
{
var args = Environment.GetCommandLineArgs();
foreach (var arg in args)
{
if (!arg.Contains('='))
{
continue;
}
var argParts = arg.Split(new[] { '=' }, 2);
if (argParts.Length < 2)
{
continue;
}
if (argParts[0] == "bitwardenServerUrl")
{
BitwardenServerUrl.Text = argParts[1];
continue;
}
if (argParts[0] == "bitwardenEmail")
{
BitwardenEmail.Text = argParts[1];
continue;
}
if (argParts[0] == "bitwardenApiKey")
{
BitwardenApiKeyOption.IsChecked = argParts[1] == "1";
continue;
}
if (argParts[0] == "bitwardenApiKeyClientId")
{
BitwardenApiKeyClientId.Text = argParts[1];
continue;
}
if (argParts[0] == "bitwardenApiKeySecret")
{
BitwardenApiKeySecret.Text = argParts[1];
continue;
}
if (argParts[0] == "bitwardenMasterPassword")
{
BitwardenPassword.Text = argParts[1];
continue;
}
if (argParts[0] == "bitwardenKeyConnector")
{
BitwardenKeyConnector.IsChecked = argParts[1] == "1";
continue;
}
if (argParts[0] == "lastpassEmail")
{
LastPassEmail.Text = argParts[1];
continue;
}
if (argParts[0] == "lastpassMasterPassword")
{
LastPassPassword.Text = argParts[1];
continue;
}
if (argParts[0] == "lastpassSkipShared")
{
LastPassSkipShared.IsChecked = argParts[1] == "1";
continue;
}
if (argParts[0] == "disableLastpassSkipShared")
{
LastPassSkipShared.IsEnabled = argParts[1] != "1";
continue;
}
if (argParts[0] == "1passwordEmail")
{
OnePasswordEmail.Text = argParts[1];
continue;
}
if (argParts[0] == "1passwordPassword")
{
OnePasswordPassword.Text = argParts[1];
continue;
}
if (argParts[0] == "1passwordSecretKey")
{
OnePasswordSecretKey.Text = argParts[1];
continue;
}
if (argParts[0] == "1passwordDomain")
{
OnePasswordDomain.Text = argParts[1];
continue;
}
if (argParts[0] == "1passwordExcludedVaults")
{
OnePasswordExcludedVaults.Text = argParts[1];
continue;
}
}
}
private void Log(string message)
{
if (_doLogging)
{
File.AppendAllText(Path.Combine(_cacheDir, "log.txt"),
$"[{DateTime.UtcNow}] {message}");
}
}
private async Task<bool> HasLatestCliAsync()
{
var cliHashFilename = Path.Combine(_cacheDir, "bw.sha256.txt");
if (!File.Exists(cliHashFilename) || !File.Exists(ResolveCliPath()))
{
return false;
}
// Read zip hash file on disk
using var zipHashStream = File.OpenRead(cliHashFilename);
using var zipHashReader = new StreamReader(zipHashStream);
var zipHashHex = zipHashReader.ReadToEnd().Trim();
// Download hash from latest CLI release
var cliHashUrl = string.Format(
"{0}/cli-v{1}/bw-{2}-sha256-{1}.txt",
_cliBaseDownloadUrl, _cliVersion, DeviceInfo.Platform == DevicePlatform.WinUI ? "windows" : "macos");
using var hashStream = await _httpClient.GetStreamAsync(cliHashUrl);
using var reader = new StreamReader(hashStream);
var hashHex = reader.ReadToEnd().Trim();
// Close streams
zipHashStream.Close();
hashStream.Close();
// Compare the hashes
return string.Equals(zipHashHex, hashHex, StringComparison.InvariantCultureIgnoreCase);
}
private async Task DownloadFileAsync(string url, string file)
{
using var stream = await _httpClient.GetStreamAsync(url);
using var fileStream = File.Create(file);
await stream.CopyToAsync(fileStream);
stream.Close();
fileStream.Close();
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>

View File

@ -6,7 +6,7 @@
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity Name="bitwarden-importer" Publisher="CN=8bit Solutions LLC, O=8bit Solutions LLC, L=Jacksonville, S=Florida, C=US, SERIALNUMBER=L16000106119, OID.1.3.6.1.4.1.311.60.2.1.2=Florida, OID.1.3.6.1.4.1.311.60.2.1.3=US, OID.2.5.4.15=Private Organization" Version="1.0.0.0" />
<Identity Name="bitwarden-importer" Publisher="CN=8bit Solutions LLC, O=8bit Solutions LLC, L=Jacksonville, S=Florida, C=US, SERIALNUMBER=L16000106119, OID.1.3.6.1.4.1.311.60.2.1.2=Florida, OID.1.3.6.1.4.1.311.60.2.1.3=US, OID.2.5.4.15=Private Organization" Version="1.5.0.0" />
<mp:PhoneIdentity PhoneProductId="569357BF-7B0D-4BDB-B134-0D3A5765BA50" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>

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

@ -1,8 +1,9 @@
using CsvHelper.Configuration.Attributes;
using PasswordManagerAccess.LastPass;
using PasswordManagerAccess.LastPass;
using System.Runtime.Serialization;
namespace Bit.Importer.Services.LastPass;
[DataContract]
public class ExportedAccount
{
public ExportedAccount() { }
@ -12,27 +13,27 @@ public class ExportedAccount
Url = account.Url;
Username = account.Username;
Password = account.Password;
// Totp not supported
// Extra = account.Notes;
Totp = account.Totp;
Extra = account.Notes;
Name = account.Name;
Grouping = account.Path == "(none)" ? null : account.Path;
// Fav = account.Favorite == "1" ? 1 : 0;
Fav = account.IsFavorite ? 1 : 0;
}
[Name("url")]
[DataMember(Name = "url")]
public string Url { get; set; }
[Name("username")]
[DataMember(Name = "username")]
public string Username { get; set; }
[Name("password")]
[DataMember(Name = "password")]
public string Password { get; set; }
[Name("totp")]
[DataMember(Name = "totp")]
public string Totp { get; set; }
[Name("extra")]
[DataMember(Name = "extra")]
public string Extra { get; set; }
[Name("name")]
[DataMember(Name = "name")]
public string Name { get; set; }
[Name("grouping")]
[DataMember(Name = "grouping")]
public string Grouping { get; set; }
[Name("fav")]
[DataMember(Name = "fav")]
public int Fav { get; set; }
}

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