Add dashlane importer and unittest (#811)

This commit is contained in:
Leendert de Borst
2025-04-28 17:33:53 +02:00
committed by Leendert de Borst
parent 89534bf78e
commit 901caa896b
9 changed files with 243 additions and 5 deletions

View File

@@ -87,7 +87,7 @@
<p>@DuplicateCredentialsCount duplicate credential(s) were found and will not be imported.</p>
</div>
}
@if (ImportedCredentials.Count == 0)
{
<div class="p-4 mb-4 text-amber-700 bg-amber-100 rounded-lg dark:bg-amber-800/30 dark:text-amber-300" role="alert">
@@ -369,7 +369,7 @@
ImportError = null;
ImportSuccessMessage = null;
StateHasChanged();
// Let UI update to start showing the loading indicator
await Task.Delay(50);
}
@@ -479,6 +479,12 @@
return;
}
if (e.File.Name.EndsWith(".zip"))
{
ImportError = $"Please unzip the {ServiceName} export file before importing, please read the instructions below for more information.";
return;
}
try
{
IsImporting = true;
@@ -500,10 +506,10 @@
await Task.WhenAll(processingTask, delayTask);
ImportedCredentials = await processingTask;
// Detect and remove duplicates before showing the preview
await DetectAndRemoveDuplicates();
CurrentStep = ImportStep.Preview;
}
catch (Exception ex)
@@ -532,7 +538,7 @@
)).ToList();
DuplicateCredentialsCount = duplicates.Count;
// Remove duplicates from the import list
ImportedCredentials = ImportedCredentials.Except(duplicates).ToList();
}

View File

@@ -0,0 +1,21 @@
@using AliasVault.ImportExport.Models
@using AliasVault.ImportExport.Importers
@inject NavigationManager NavigationManager
@inject GlobalNotificationService GlobalNotificationService
@inject ILogger<ImportServiceBitwarden> Logger
<ImportServiceCard
ServiceName="Dashlane"
Description="Import passwords from your Dashlane account"
LogoUrl="img/importers/dashlane.svg"
ProcessFileCallback="ProcessFile">
<p class="text-gray-700 dark:text-gray-300 mb-4">In order to import your Dashlane passwords, you need to export it as a CSV file. You can do this by logging into your Dashlane account, going to the 'Account' > 'Settings' menu and selecting 'Export to CSV'.</p>
<p class="text-gray-700 dark:text-gray-300 mb-4">Note: the .zip file you download will contain a "credentials.csv" file. You need to unzip the archive first, and then upload the "credentials.csv" CSV file below.</p>
</ImportServiceCard>
@code {
private static async Task<List<ImportedCredential>> ProcessFile(string fileContents)
{
return await DashlaneImporter.ImportFromCsvAsync(fileContents);
}
}

View File

@@ -24,6 +24,7 @@
<ImportService1Password />
<ImportServiceBitwarden />
<ImportServiceChrome />
<ImportServiceDashlane />
<ImportServiceFirefox />
<ImportServiceKeePass />
<ImportServiceKeePassXC />

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<circle cx="512" cy="512" r="512" style="fill:#10353e"/>
<path d="m544.7 458.9 53.8 19.8c8.7 3.1 19.3-1 19.3-7.5V334.9c0-3.1-2.6-6-6.8-7.5l-53.8-19.8c-8.7-3.1-19.3 1-19.3 7.5v136.4c0 3.1 2.6 5.9 6.8 7.4m0 244.8 53.8 19.8c8.7 3.1 19.3-1 19.3-7.5V579.7c0-3.1-2.6-6-6.8-7.5l-53.8-19.8c-8.7-3.1-19.3 1-19.3 7.5v136.4c0 3.1 2.6 6 6.8 7.4M445 413.3l53.8 19.8c8.7 3.1 19.3-1 19.3-7.5V277.3c0-3.1-2.6-6-6.8-7.5L457.5 250c-8.7-3.1-19.3 1-19.3 7.5v148.4c0 3.1 2.6 5.9 6.8 7.4m0 326.9 53.8 19.8c8.7 3.1 19.3-1 19.3-7.5v-148c0-3.1-2.6-6-6.8-7.5l-53.8-19.8c-8.7-3.1-19.3 1-19.3 7.5v148c0 3.1 2.6 6 6.8 7.5m-26.6-457.4c0-3.1-2.6-6-6.8-7.5l-53.8-19.8c-8.7-3.1-19.3 1-19.3 7.5v464.1c0 3.1 2.6 6 6.8 7.5l53.8 19.8c8.7 3.1 19.3-1 19.3-7.5V282.8zM710.7 406l-53.8-19.8c-8.7-3.1-19.3 1-19.3 7.5v224c0 3.1 2.6 6 6.8 7.5l53.8 19.8c8.7 3.1 19.3-1 19.3-7.5v-224c0-3.1-2.6-6-6.8-7.5" style="fill:#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -69,6 +69,7 @@
<EmbeddedResource Include="TestData\Exports\keepassxc.csv" />
<EmbeddedResource Include="TestData\Exports\1password_8.csv" />
<EmbeddedResource Include="TestData\Exports\protonpass.csv" />
<EmbeddedResource Include="TestData\Exports\dashlane.csv" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,4 @@
username,username2,username3,title,password,note,url,category,otpUrl
Test username,,,Test,password123,,https://Test,,
googleuser,,,Google,googlepassword,,https://www.google.com,,
testusername,testusernamealternative,,Local,testpassword,testnote,https://www.testwebsite.local,,
1 username username2 username3 title password note url category otpUrl
2 Test username Test password123 https://Test
3 googleuser Google googlepassword https://www.google.com
4 testusername testusernamealternative Local testpassword testnote https://www.testwebsite.local

View File

@@ -406,4 +406,52 @@ public class ImportExportTests
Assert.That(testWithEmailCredential.Email, Is.EqualTo("testalias.gating981@passinbox.com"));
});
}
/// <summary>
/// Test case for importing credentials from Dashlane CSV and ensuring all values are present.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task ImportCredentialsFromDashlaneCsv()
{
// Arrange
var fileContent = await ResourceReaderUtility.ReadEmbeddedResourceStringAsync("AliasVault.UnitTests.TestData.Exports.dashlane.csv");
// Act
var importedCredentials = await DashlaneImporter.ImportFromCsvAsync(fileContent);
// Assert
Assert.That(importedCredentials, Has.Count.EqualTo(3));
// Test specific entries
var testCredential = importedCredentials.First(c => c.ServiceName == "Test");
Assert.Multiple(() =>
{
Assert.That(testCredential.ServiceName, Is.EqualTo("Test"));
Assert.That(testCredential.ServiceUrl, Is.EqualTo("https://Test"));
Assert.That(testCredential.Username, Is.EqualTo("Test username"));
Assert.That(testCredential.Password, Is.EqualTo("password123"));
Assert.That(testCredential.Notes, Is.Null);
});
var googleCredential = importedCredentials.First(c => c.ServiceName == "Google");
Assert.Multiple(() =>
{
Assert.That(googleCredential.ServiceName, Is.EqualTo("Google"));
Assert.That(googleCredential.ServiceUrl, Is.EqualTo("https://www.google.com"));
Assert.That(googleCredential.Username, Is.EqualTo("googleuser"));
Assert.That(googleCredential.Password, Is.EqualTo("googlepassword"));
Assert.That(googleCredential.Notes, Is.Null);
});
var localCredential = importedCredentials.First(c => c.ServiceName == "Local");
Assert.Multiple(() =>
{
Assert.That(localCredential.ServiceName, Is.EqualTo("Local"));
Assert.That(localCredential.ServiceUrl, Is.EqualTo("https://www.testwebsite.local"));
Assert.That(localCredential.Username, Is.EqualTo("testusername"));
Assert.That(localCredential.Password, Is.EqualTo("testpassword"));
Assert.That(localCredential.Notes, Is.EqualTo("testnote\nAlternative username 1: testusernamealternative"));
});
}
}

View File

@@ -0,0 +1,81 @@
//-----------------------------------------------------------------------
// <copyright file="DashlaneImporter.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.ImportExport.Importers;
using AliasVault.ImportExport.Models;
using AliasVault.ImportExport.Models.Imports;
using CsvHelper;
using CsvHelper.Configuration;
using System.Globalization;
/// <summary>
/// Imports credentials from Dashlane.
/// </summary>
public static class DashlaneImporter
{
/// <summary>
/// Imports Dashlane CSV file and converts contents to list of ImportedCredential model objects.
/// </summary>
/// <param name="fileContent">The content of the CSV file.</param>
/// <returns>The imported list of ImportedCredential objects.</returns>
public static async Task<List<ImportedCredential>> ImportFromCsvAsync(string fileContent)
{
using var reader = new StringReader(fileContent);
using var csv = new CsvReader(reader, new CsvConfiguration(CultureInfo.InvariantCulture));
var credentials = new List<ImportedCredential>();
await foreach (var record in csv.GetRecordsAsync<DashlaneCsvRecord>())
{
var credential = new ImportedCredential
{
ServiceName = record.Title,
ServiceUrl = record.URL,
Username = record.Username,
Password = record.Password,
TwoFactorSecret = record.OTPUrl,
Notes = BuildNotes(record)
};
credentials.Add(credential);
}
if (credentials.Count == 0)
{
throw new InvalidOperationException("No records found in the CSV file.");
}
return credentials;
}
private static string? BuildNotes(DashlaneCsvRecord record)
{
var notes = new List<string>();
if (!string.IsNullOrEmpty(record.Note))
{
notes.Add(record.Note);
}
if (!string.IsNullOrEmpty(record.Username2))
{
notes.Add($"Alternative username 1: {record.Username2}");
}
if (!string.IsNullOrEmpty(record.Username3))
{
notes.Add($"Alternative username 2: {record.Username3}");
}
if (!string.IsNullOrEmpty(record.Category))
{
notes.Add($"Category: {record.Category}");
}
return notes.Count > 0 ? string.Join(Environment.NewLine, notes) : null;
}
}

View File

@@ -0,0 +1,71 @@
//-----------------------------------------------------------------------
// <copyright file="DashlaneCsvRecord.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
using AliasVault.ImportExport.Converters;
using CsvHelper.Configuration.Attributes;
namespace AliasVault.ImportExport.Models.Imports;
/// <summary>
/// Represents a Dashlane CSV record that is being imported from a Dashlane CSV export file.
/// </summary>
public class DashlaneCsvRecord
{
/// <summary>
/// Gets or sets the primary username.
/// </summary>
[Name("username")]
public string Username { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the second username.
/// </summary>
[Name("username2")]
public string? Username2 { get; set; }
/// <summary>
/// Gets or sets the third username.
/// </summary>
[Name("username3")]
public string? Username3 { get; set; }
/// <summary>
/// Gets or sets the title/service name.
/// </summary>
[Name("title")]
public string Title { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the password.
/// </summary>
[Name("password")]
public string? Password { get; set; }
/// <summary>
/// Gets or sets any additional notes.
/// </summary>
[Name("note")]
public string? Note { get; set; }
/// <summary>
/// Gets or sets the service URL.
/// </summary>
[Name("url")]
public string? URL { get; set; }
/// <summary>
/// Gets or sets the category.
/// </summary>
[Name("category")]
public string? Category { get; set; }
/// <summary>
/// Gets or sets the OTP URL.
/// </summary>
[Name("otpUrl")]
public string? OTPUrl { get; set; }
}