Add NordPass import method (#1474)

This commit is contained in:
Leendert de Borst
2026-01-27 19:34:08 +01:00
committed by Leendert de Borst
parent d44319feaf
commit 215835340a
10 changed files with 492 additions and 10 deletions

View File

@@ -0,0 +1,25 @@
@using AliasVault.ImportExport.Models
@using AliasVault.ImportExport.Importers
@using Microsoft.Extensions.Localization
@inject NavigationManager NavigationManager
@inject GlobalNotificationService GlobalNotificationService
@inject IStringLocalizerFactory LocalizerFactory
@inject ILogger<ImportServiceNordPass> Logger
<ImportServiceCard
ServiceName="NordPass"
Description="@Localizer["NordPassDescription"]"
LogoUrl="img/importers/nordpass.svg"
ProcessFileCallback="ProcessFile">
<p class="text-gray-700 dark:text-gray-300 mb-4">@Localizer["NordPassInstructionsPart1"]</p>
<p class="text-gray-700 dark:text-gray-300 mb-4">@Localizer["NordPassInstructionsPart2"]</p>
</ImportServiceCard>
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Settings.ImportExport.ImportServices", "AliasVault.Client");
private static async Task<List<ImportedCredential>> ProcessFile(string fileContents)
{
return await NordPassImporter.ImportFromCsvAsync(fileContents);
}
}

View File

@@ -31,6 +31,7 @@
<ImportServiceKeePass />
<ImportServiceKeePassXC />
<ImportServiceLastPass />
<ImportServiceNordPass />
<ImportServiceProtonPass />
<ImportServiceStrongbox />
<ImportServiceAliasVault />

View File

@@ -191,6 +191,19 @@
<value>Once you have exported the file, you can upload it below.</value>
<comment>KeePassXC export instructions part 2</comment>
</data>
<!-- NordPass -->
<data name="NordPassDescription" xml:space="preserve">
<value>Import passwords from NordPass</value>
<comment>Description for NordPass import service</comment>
</data>
<data name="NordPassInstructionsPart1" xml:space="preserve">
<value>In order to import your NordPass passwords, you need to export them as a CSV file. You can do this by opening the NordPass app or web vault, going to 'Settings' > 'Export Items', and selecting the CSV format.</value>
<comment>NordPass export instructions part 1</comment>
</data>
<data name="NordPassInstructionsPart2" xml:space="preserve">
<value>Once you have exported the file, you can upload it below.</value>
<comment>NordPass export instructions part 2</comment>
</data>
<!-- Proton Pass -->
<data name="ProtonPassDescription" xml:space="preserve">
<value>Import passwords from Proton Pass</value>

View File

@@ -2712,6 +2712,11 @@ video {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.hover\:border-gray-300:hover {
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity));
}
.hover\:border-orange-400:hover {
--tw-border-opacity: 1;
border-color: rgb(251 146 60 / var(--tw-border-opacity));
@@ -2727,11 +2732,6 @@ video {
border-color: rgb(244 149 65 / var(--tw-border-opacity));
}
.hover\:border-gray-300:hover {
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity));
}
.hover\:bg-blue-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
@@ -3622,6 +3622,11 @@ video {
color: rgb(107 114 128 / var(--tw-text-opacity));
}
.dark\:hover\:border-gray-500:hover:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(107 114 128 / var(--tw-border-opacity));
}
.dark\:hover\:border-orange-500:hover:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(249 115 22 / var(--tw-border-opacity));
@@ -3632,11 +3637,6 @@ video {
border-color: rgb(244 149 65 / var(--tw-border-opacity));
}
.dark\:hover\:border-gray-500:hover:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(107 114 128 / var(--tw-border-opacity));
}
.dark\:hover\:bg-blue-500:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2"><path d="M506 116v280c0 60.71-49.29 110-110 110H116C55.29 506 6 456.71 6 396V116C6 55.289 55.29 6 116 6h280c60.71 0 110 49.289 110 110z" fill="#00cfb6"/><path d="M89.05 290.616a167.348 167.348 0 0031.783 98.434l80.131-130.713 28.885 49.582-7.936-36.887 34.082-57.808 55.107 94.695-7.862-36.523 7.75-13.124 80.177 130.778a167.348 167.348 0 0031.783-98.434c0-92.601-74.746-167.665-166.955-167.665-92.198-.019-166.945 75.055-166.945 167.665" fill="#fff" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 626 B

View File

@@ -72,6 +72,7 @@
<EmbeddedResource Include="TestData\Exports\1password_8.csv" />
<EmbeddedResource Include="TestData\Exports\protonpass.csv" />
<EmbeddedResource Include="TestData\Exports\lastpass.csv" />
<EmbeddedResource Include="TestData\Exports\nordpass.csv" />
<EmbeddedResource Include="TestData\Exports\aliasvault_mobile_app_export.csv" />
</ItemGroup>

View File

@@ -0,0 +1,8 @@
name,url,additional_urls,username,password,note,cardholdername,cardnumber,cvc,pin,expirydate,zipcode,folder,full_name,phone_number,email,address1,address2,city,country,state,type,custom_fields
Password title,http://google.nl,,email@example.tld,password,,,,,,,,Business,,,,,,,,,password,"[{""type"":""text"",""label"":""CustomFieldName1"",""value"":""Test""}]"
SecureNote1,,,,,"This is my secure note content
Test test",,,,,,,,,,,,,,,,note,
Creditcard Visa,,,,,,Holdername,1234123412341234123,1231,1231,12/28,1231AB,,,,,,,,,,credit_card,
Business,,,,,,,,,,,,,,,,,,,,,folder,
Root item,,,rootuser,rootpass,,,,,,,,,,,,,,,,,password,
1 name url additional_urls username password note cardholdername cardnumber cvc pin expirydate zipcode folder full_name phone_number email address1 address2 city country state type custom_fields
2 Password title http://google.nl email@example.tld password Business password [{"type":"text","label":"CustomFieldName1","value":"Test"}]
3 SecureNote1 This is my secure note content Test test note
4 Creditcard Visa Holdername 1234123412341234123 1231 1231 12/28 1231AB credit_card
5 Business folder
6 Root item rootuser rootpass password

View File

@@ -667,6 +667,135 @@ public class ImportExportTests
});
}
/// <summary>
/// Test case for importing credentials from NordPass CSV and ensuring all values are present.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task ImportCredentialsFromNordPassCsv()
{
// Arrange
var fileContent = await ResourceReaderUtility.ReadEmbeddedResourceStringAsync("AliasVault.UnitTests.TestData.Exports.nordpass.csv");
// Act
var importedCredentials = await NordPassImporter.ImportFromCsvAsync(fileContent);
// Assert - Should import 4 records (folder entry is skipped)
Assert.That(importedCredentials, Has.Count.EqualTo(4));
// Test regular password credential
var passwordCredential = importedCredentials.First(c => c.ServiceName == "Password title");
Assert.Multiple(() =>
{
Assert.That(passwordCredential.ServiceName, Is.EqualTo("Password title"));
Assert.That(passwordCredential.ServiceUrl, Is.EqualTo("http://google.nl"));
Assert.That(passwordCredential.Username, Is.EqualTo("email@example.tld"));
Assert.That(passwordCredential.Password, Is.EqualTo("password"));
Assert.That(passwordCredential.FolderPath, Is.EqualTo("Business"));
Assert.That(passwordCredential.ItemType, Is.EqualTo(ImportedItemType.Login));
Assert.That(passwordCredential.Notes, Does.Contain("[{\"type\":\"text\",\"label\":\"CustomFieldName1\",\"value\":\"Test\"}]"));
});
// Test secure note
var secureNote = importedCredentials.First(c => c.ServiceName == "SecureNote1");
Assert.Multiple(() =>
{
Assert.That(secureNote.ServiceName, Is.EqualTo("SecureNote1"));
Assert.That(secureNote.ServiceUrl, Is.Null);
Assert.That(secureNote.Username, Is.Empty);
Assert.That(secureNote.Password, Is.Empty);
Assert.That(secureNote.ItemType, Is.EqualTo(ImportedItemType.Note));
Assert.That(secureNote.Notes, Does.Contain("This is my secure note content"));
Assert.That(secureNote.Notes, Does.Contain("Test test"));
});
// Test credit card
var creditCard = importedCredentials.First(c => c.ServiceName == "Creditcard Visa");
Assert.Multiple(() =>
{
Assert.That(creditCard.ServiceName, Is.EqualTo("Creditcard Visa"));
Assert.That(creditCard.ItemType, Is.EqualTo(ImportedItemType.Creditcard));
Assert.That(creditCard.Creditcard, Is.Not.Null);
Assert.That(creditCard.Creditcard!.CardholderName, Is.EqualTo("Holdername"));
Assert.That(creditCard.Creditcard.Number, Is.EqualTo("1234123412341234123"));
Assert.That(creditCard.Creditcard.Cvv, Is.EqualTo("1231"));
Assert.That(creditCard.Creditcard.Pin, Is.EqualTo("1231"));
Assert.That(creditCard.Creditcard.ExpiryMonth, Is.EqualTo("12"));
Assert.That(creditCard.Creditcard.ExpiryYear, Is.EqualTo("28"));
});
// Test root item (no folder)
var rootItem = importedCredentials.First(c => c.ServiceName == "Root item");
Assert.Multiple(() =>
{
Assert.That(rootItem.ServiceName, Is.EqualTo("Root item"));
Assert.That(rootItem.Username, Is.EqualTo("rootuser"));
Assert.That(rootItem.Password, Is.EqualTo("rootpass"));
Assert.That(rootItem.FolderPath, Is.Null);
Assert.That(rootItem.ItemType, Is.EqualTo(ImportedItemType.Login));
});
}
/// <summary>
/// Test case for NordPass folder import.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task NordPassFolderImport()
{
// Arrange
var fileContent = await ResourceReaderUtility.ReadEmbeddedResourceStringAsync("AliasVault.UnitTests.TestData.Exports.nordpass.csv");
// Act
var importedCredentials = await NordPassImporter.ImportFromCsvAsync(fileContent);
// Assert - verify folder path is extracted
var folderNames = BaseImporter.CollectUniqueFolderNames(importedCredentials);
Assert.That(folderNames, Does.Contain("Business"));
var credentialWithFolder = importedCredentials.First(c => c.FolderPath == "Business");
Assert.That(credentialWithFolder.ServiceName, Is.EqualTo("Password title"));
}
/// <summary>
/// Test case for NordPass credit card detection and parsing.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task NordPassCreditCardDetectionAndParsing()
{
// Arrange
var fileContent = await ResourceReaderUtility.ReadEmbeddedResourceStringAsync("AliasVault.UnitTests.TestData.Exports.nordpass.csv");
// Act
var importedCredentials = await NordPassImporter.ImportFromCsvAsync(fileContent);
// Assert - verify credit card is detected and parsed
var creditCardCredential = importedCredentials.First(c => c.ServiceName == "Creditcard Visa");
Assert.That(creditCardCredential.ItemType, Is.EqualTo(ImportedItemType.Creditcard));
Assert.That(creditCardCredential.Creditcard, Is.Not.Null);
Assert.That(creditCardCredential.Creditcard!.CardholderName, Is.EqualTo("Holdername"));
Assert.That(creditCardCredential.Creditcard.Number, Is.EqualTo("1234123412341234123"));
Assert.That(creditCardCredential.Creditcard.Cvv, Is.EqualTo("1231"));
Assert.That(creditCardCredential.Creditcard.Pin, Is.EqualTo("1231"));
Assert.That(creditCardCredential.Creditcard.ExpiryMonth, Is.EqualTo("12"));
Assert.That(creditCardCredential.Creditcard.ExpiryYear, Is.EqualTo("28"));
// Convert to item and verify fields
var items = BaseImporter.ConvertToItem([creditCardCredential]);
var creditCardItem = items[0];
Assert.That(creditCardItem.ItemType, Is.EqualTo(ItemType.CreditCard));
var cardNumber = creditCardItem.FieldValues.FirstOrDefault(fv => fv.FieldKey == FieldKey.CardNumber);
Assert.That(cardNumber?.Value, Is.EqualTo("1234123412341234123"));
var cardholderName = creditCardItem.FieldValues.FirstOrDefault(fv => fv.FieldKey == FieldKey.CardCardholderName);
Assert.That(cardholderName?.Value, Is.EqualTo("Holdername"));
var cardPin = creditCardItem.FieldValues.FirstOrDefault(fv => fv.FieldKey == FieldKey.CardPin);
Assert.That(cardPin?.Value, Is.EqualTo("1231"));
}
/// <summary>
/// Test case for importing credentials from AliasVault Mobile App CSV export and ensuring all values are present.
/// </summary>

View File

@@ -0,0 +1,150 @@
//-----------------------------------------------------------------------
// <copyright file="NordPassImporter.cs" company="aliasvault">
// Copyright (c) aliasvault. All rights reserved.
// Licensed under the AGPLv3 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;
/// <summary>
/// Imports credentials from NordPass.
/// </summary>
public static class NordPassImporter
{
/// <summary>
/// Imports NordPass 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)
{
var records = await BaseImporter.ImportCsvDataAsync<NordPassCsvRecord>(fileContent);
var credentials = new List<ImportedCredential>();
foreach (var record in records)
{
// Skip folder entries - NordPass exports folder rows with type "folder" which are not credentials.
if (string.Equals(record.Type, "folder", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var itemType = MapNordPassType(record.Type);
ImportedCreditcard? creditcard = null;
// Parse credit card data if any credit card fields are present.
if (HasCreditcardData(record))
{
creditcard = ParseCreditcard(record);
itemType ??= ImportedItemType.Creditcard;
}
// Build notes, appending custom fields if present.
var notes = record.Note;
if (!string.IsNullOrWhiteSpace(record.CustomFields))
{
notes = string.IsNullOrWhiteSpace(notes)
? record.CustomFields
: notes + Environment.NewLine + record.CustomFields;
}
var credential = new ImportedCredential
{
ServiceName = record.Name,
ServiceUrl = string.IsNullOrWhiteSpace(record.Url) ? null : record.Url,
Email = record.Email,
Username = record.Username,
Password = record.Password,
Notes = notes,
FolderPath = string.IsNullOrWhiteSpace(record.Folder) ? null : record.Folder,
ItemType = itemType,
Creditcard = creditcard,
};
credentials.Add(credential);
}
return credentials;
}
/// <summary>
/// Maps NordPass type values to ImportedItemType.
/// NordPass types: password, note, credit_card, identity.
/// </summary>
private static ImportedItemType? MapNordPassType(string? nordPassType)
{
if (string.IsNullOrWhiteSpace(nordPassType))
{
return null;
}
return nordPassType.ToLowerInvariant() switch
{
"password" => ImportedItemType.Login,
"note" => ImportedItemType.Note,
"credit_card" => ImportedItemType.Creditcard,
"identity" => ImportedItemType.Alias,
_ => ImportedItemType.Login,
};
}
/// <summary>
/// Checks whether the NordPass record contains credit card data.
/// </summary>
private static bool HasCreditcardData(NordPassCsvRecord record)
{
return !string.IsNullOrWhiteSpace(record.CardNumber) ||
!string.IsNullOrWhiteSpace(record.CardholderName) ||
!string.IsNullOrWhiteSpace(record.Cvc) ||
!string.IsNullOrWhiteSpace(record.ExpiryDate);
}
/// <summary>
/// Parses credit card data from a NordPass record.
/// </summary>
private static ImportedCreditcard ParseCreditcard(NordPassCsvRecord record)
{
var creditcard = new ImportedCreditcard
{
CardholderName = record.CardholderName,
Number = record.CardNumber,
Cvv = record.Cvc,
Pin = record.Pin,
};
// Parse expiry date - NordPass uses various formats, commonly "MM/YYYY" or "MMYYYY".
if (!string.IsNullOrWhiteSpace(record.ExpiryDate))
{
ParseExpiryDate(record.ExpiryDate, creditcard);
}
return creditcard;
}
/// <summary>
/// Parses NordPass expiry date string into month and year components.
/// Handles formats like "MM/YYYY", "MM/YY", and "MMYYYY".
/// </summary>
private static void ParseExpiryDate(string expiryDate, ImportedCreditcard creditcard)
{
if (expiryDate.Contains('/'))
{
var parts = expiryDate.Split('/');
if (parts.Length == 2)
{
creditcard.ExpiryMonth = parts[0].Trim().PadLeft(2, '0');
creditcard.ExpiryYear = parts[1].Trim();
}
}
else if (expiryDate.Length == 6)
{
// Format: MMYYYY
creditcard.ExpiryMonth = expiryDate[..2];
creditcard.ExpiryYear = expiryDate[2..];
}
}
}

View File

@@ -0,0 +1,154 @@
//-----------------------------------------------------------------------
// <copyright file="NordPassCsvRecord.cs" company="aliasvault">
// Copyright (c) aliasvault. All rights reserved.
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.ImportExport.Models.Imports;
using CsvHelper.Configuration.Attributes;
/// <summary>
/// Represents a NordPass CSV record that is being imported from a NordPass CSV export file.
/// </summary>
public class NordPassCsvRecord
{
/// <summary>
/// Gets or sets the name of the item.
/// </summary>
[Name("name")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the URL of the item.
/// </summary>
[Name("url")]
public string Url { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the additional URLs of the item.
/// </summary>
[Name("additional_urls")]
public string? AdditionalUrls { get; set; }
/// <summary>
/// Gets or sets the username of the item.
/// </summary>
[Name("username")]
public string Username { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the password of the item.
/// </summary>
[Name("password")]
public string Password { get; set; } = string.Empty;
/// <summary>
/// Gets or sets any additional notes.
/// </summary>
[Name("note")]
public string? Note { get; set; }
/// <summary>
/// Gets or sets the cardholder name (for credit card items).
/// </summary>
[Name("cardholdername")]
public string? CardholderName { get; set; }
/// <summary>
/// Gets or sets the card number (for credit card items).
/// </summary>
[Name("cardnumber")]
public string? CardNumber { get; set; }
/// <summary>
/// Gets or sets the CVC/CVV code (for credit card items).
/// </summary>
[Name("cvc")]
public string? Cvc { get; set; }
/// <summary>
/// Gets or sets the PIN (for credit card items).
/// </summary>
[Name("pin")]
public string? Pin { get; set; }
/// <summary>
/// Gets or sets the expiry date (for credit card items).
/// </summary>
[Name("expirydate")]
public string? ExpiryDate { get; set; }
/// <summary>
/// Gets or sets the zip code (for identity items).
/// </summary>
[Name("zipcode")]
public string? ZipCode { get; set; }
/// <summary>
/// Gets or sets the folder name.
/// </summary>
[Name("folder")]
public string? Folder { get; set; }
/// <summary>
/// Gets or sets the full name (for identity items).
/// </summary>
[Name("full_name")]
public string? FullName { get; set; }
/// <summary>
/// Gets or sets the phone number (for identity items).
/// </summary>
[Name("phone_number")]
public string? PhoneNumber { get; set; }
/// <summary>
/// Gets or sets the email address (for identity items).
/// </summary>
[Name("email")]
public string? Email { get; set; }
/// <summary>
/// Gets or sets the first address line (for identity items).
/// </summary>
[Name("address1")]
public string? Address1 { get; set; }
/// <summary>
/// Gets or sets the second address line (for identity items).
/// </summary>
[Name("address2")]
public string? Address2 { get; set; }
/// <summary>
/// Gets or sets the city (for identity items).
/// </summary>
[Name("city")]
public string? City { get; set; }
/// <summary>
/// Gets or sets the country (for identity items).
/// </summary>
[Name("country")]
public string? Country { get; set; }
/// <summary>
/// Gets or sets the state (for identity items).
/// </summary>
[Name("state")]
public string? State { get; set; }
/// <summary>
/// Gets or sets the type of the item (e.g., password, note, card, identity).
/// </summary>
[Name("type")]
public string? Type { get; set; }
/// <summary>
/// Gets or sets custom fields as a JSON or delimited string.
/// </summary>
[Name("custom_fields")]
public string? CustomFields { get; set; }
}