Add 1Password importer (#542)

This commit is contained in:
Leendert de Borst
2025-03-29 16:41:19 +01:00
committed by Leendert de Borst
parent 43f5e0c647
commit 6f5ae7c17e
10 changed files with 378 additions and 51 deletions

View File

@@ -0,0 +1,21 @@
@using AliasVault.ImportExport.Models
@using AliasVault.ImportExport.Importers
@inject NavigationManager NavigationManager
@inject GlobalNotificationService GlobalNotificationService
@inject ILogger<ImportService1Password> Logger
<ImportServiceCard
ServiceName="1Password"
Description="Import passwords from your 1Password vault"
LogoUrl="img/importers/1password.svg"
ProcessFileCallback="ProcessFile">
<p class="text-gray-700 dark:text-gray-300 mb-4">In order to import your 1Password vault, you need to export it as a CSV file. You can do this by logging into your 1Password account in the 1Password 8 desktop app (Windows / MacOS / Linux), going to the 'File' menu and selecting 'Export' (to CSV).</p>
<p class="text-gray-700 dark:text-gray-300 mb-4">Once you have exported the file, you can upload it below.</p>
</ImportServiceCard>
@code {
private async Task<List<ImportedCredential>> ProcessFile(string fileContents)
{
return await OnePasswordImporter.ImportFromCsvAsync(fileContents);
}
}

View File

@@ -39,7 +39,7 @@
<ClickOutsideHandler OnClose="CloseModal" ContentId="importServiceModal">
<ModalWrapper OnEnter="HandleModalConfirm">
<div id="importServiceModal" class="relative top-20 mx-auto p-5 shadow-lg rounded-md bg-white dark:bg-gray-800 border-2 border-gray-300 dark:border-gray-400 md:min-w-[32rem]">
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 max-w-lg w-full mx-auto">
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 w-full mx-auto">
<div class="flex justify-between items-center mb-4">
<div class="flex"><img src="@LogoUrl" alt="@ServiceName logo" class="w-8 h-8 float-left mr-4" /><h3 class="text-xl font-semibold dark:text-white">Import from @ServiceName</h3></div>
<button @onclick="CloseModal" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300">
@@ -55,26 +55,28 @@
@switch (CurrentStep)
{
case ImportStep.FileUpload:
@if (!string.IsNullOrEmpty(ImportError))
{
<div class="mb-4 p-4 text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800" role="alert">
@ImportError
</div>
}
<div class="max-w-lg mx-auto">
@if (!string.IsNullOrEmpty(ImportError))
{
<div class="mb-4 p-4 text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800" role="alert">
@ImportError
</div>
}
@if (IsImporting)
{
<LoadingIndicator />
}
@if (IsImporting)
{
<LoadingIndicator />
}
<div class="@(IsImporting ? "hidden" : "")">
@ChildContent
<div class="mb-4 bg-amber-50 border border-amber-400 dark:bg-amber-800/30 dark:border-amber-500/50 rounded-lg p-4">
<p class="mb-4 text-gray-700 dark:text-gray-200">Upload your @ServiceName export file:</p>
<InputFile OnChange="HandleFileUpload" class="text-gray-700 dark:text-gray-200 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100 dark:file:bg-primary-900/40 dark:file:text-primary-300 dark:hover:file:bg-primary-800/60" />
</div>
<div class="flex justify-end mt-6 space-x-2">
<Button OnClick="@CloseModal" Color="secondary">Cancel</Button>
<div class="@(IsImporting ? "hidden" : "")">
@ChildContent
<div class="mb-4 bg-amber-50 border border-amber-400 dark:bg-amber-800/30 dark:border-amber-500/50 rounded-lg p-4">
<p class="mb-4 text-gray-700 dark:text-gray-200">Upload your @ServiceName export file:</p>
<InputFile OnChange="HandleFileUpload" class="text-gray-700 dark:text-gray-200 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100 dark:file:bg-primary-900/40 dark:file:text-primary-300 dark:hover:file:bg-primary-800/60" />
</div>
<div class="flex justify-end mt-6 space-x-2">
<Button OnClick="@CloseModal" Color="secondary">Cancel</Button>
</div>
</div>
</div>
break;
@@ -113,19 +115,21 @@
break;
case ImportStep.Confirm:
@if (IsImporting)
{
<LoadingIndicator />
}
else {
<div class="mb-4">
<p class="mb-4 text-gray-700 dark:text-gray-300">Are you sure you want to import (@ImportedCredentials.Count) credentials? Note: the import process can take a short while.</p>
</div>
<div class="flex justify-end mt-6 space-x-2">
<Button OnClick="@HandlePreviousStep" Color="secondary">Back</Button>
<Button OnClick="@HandleModalConfirm" Color="primary">Import</Button>
</div>
}
<div class="max-w-lg mx-auto">
@if (IsImporting)
{
<LoadingIndicator />
}
else {
<div class="mb-4">
<p class="mb-4 text-gray-700 dark:text-gray-300">Are you sure you want to import (@ImportedCredentials.Count) credentials? Note: the import process can take a short while.</p>
</div>
<div class="flex justify-end mt-6 space-x-2">
<Button OnClick="@HandlePreviousStep" Color="secondary">Back</Button>
<Button OnClick="@HandleModalConfirm" Color="primary">Import</Button>
</div>
}
</div>
break;
}
</div>

View File

@@ -23,6 +23,7 @@
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<ImportServiceBitwarden />
<ImportServiceKeePass />
<ImportService1Password />
<ImportServiceAliasVault />
</div>
</div>

View File

@@ -769,10 +769,6 @@ video {
margin-left: 0.75rem;
}
.ml-6 {
margin-left: 1.5rem;
}
.ml-auto {
margin-left: auto;
}
@@ -2644,11 +2640,6 @@ video {
background-color: rgb(133 77 14 / var(--tw-bg-opacity));
}
.dark\:bg-yellow-900:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
}
.dark\:bg-opacity-80:is(.dark *) {
--tw-bg-opacity: 0.8;
}
@@ -2743,11 +2734,6 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark\:text-yellow-100:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(254 249 195 / var(--tw-text-opacity));
}
.dark\:text-yellow-400:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(250 204 21 / var(--tw-text-opacity));

View File

@@ -0,0 +1,143 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.8.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:url(#SVGID_00000003806953128981211710000010604494801610004877_);}
.st2{fill:url(#SVGID_00000138534652836814865550000015513145662374071175_);}
.st3{fill:#FFFFFF;}
.st4{fill:url(#SVGID_00000178891249291936294170000017599752083928133535_);}
.st5{fill:url(#SVGID_00000099624116082093634780000002130301463185760680_);}
.st6{fill:url(#SVGID_00000159460032060823389730000000515703250918618502_);}
.st7{fill:url(#SVGID_00000021835272673548630180000003746360465168387750_);}
.st8{fill:url(#SVGID_00000008841417857545760810000015183692108847128232_);}
.st9{fill:url(#SVGID_00000036216766700710711660000006010378259165164950_);stroke:url(#SVGID_00000057135321266455722670000016196693940672554887_);stroke-miterlimit:10;}
.st10{fill:url(#SVGID_00000006671298426127412530000003354577712219538314_);}
.st11{fill:url(#SVGID_00000019663290019837682370000004612082602279049916_);}
.st12{fill:url(#SVGID_00000027569226305715931490000007753892242520527007_);}
.st13{fill:url(#SVGID_00000158006836777100829930000014279642698594133424_);}
.st14{fill:url(#SVGID_00000034788272653732579960000016974155572991256201_);}
.st15{fill:url(#SVGID_00000160886399886325602350000012757927620993370543_);}
</style>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="256" y1="1.109584e-10" x2="256" y2="512">
<stop offset="0" style="stop-color:#CFD4E2"/>
<stop offset="1" style="stop-color:#A9B4CC"/>
</linearGradient>
<path class="st0" d="M420.1,512H91.9C41.3,512,0,470.7,0,420.1V91.9C0,41.3,41.3,0,91.9,0h328.2C470.7,0,512,41.3,512,91.9v328.2
C512,470.7,470.7,512,420.1,512z"/>
<radialGradient id="SVGID_00000080892890122459656140000004003202712423285181_" cx="255.006" cy="259.5827" r="227.9617" gradientTransform="matrix(1.006 0 0 1.0202 -1.5341 -5.0938)" gradientUnits="userSpaceOnUse">
<stop offset="0.96" style="stop-color:#000000;stop-opacity:0.7"/>
<stop offset="1" style="stop-color:#000000;stop-opacity:0"/>
</radialGradient>
<path style="fill:url(#SVGID_00000080892890122459656140000004003202712423285181_);" d="M487.3,259.1c0,129-104,234.9-232.3,234.9
S22.7,388.1,22.7,259.1S126.7,25.5,255,25.5S487.3,130.1,487.3,259.1z"/>
<linearGradient id="SVGID_00000088836898082065010640000018020030158233795000_" gradientUnits="userSpaceOnUse" x1="255" y1="31.7054" x2="255" y2="479.2018">
<stop offset="0" style="stop-color:#F7F8FA"/>
<stop offset="1" style="stop-color:#A5B1D4"/>
</linearGradient>
<circle style="fill:url(#SVGID_00000088836898082065010640000018020030158233795000_);" cx="255" cy="255.5" r="223.7"/>
<circle class="st3" cx="255" cy="255.5" r="207.5"/>
<linearGradient id="SVGID_00000069371419516710993290000014674188358113625238_" gradientUnits="userSpaceOnUse" x1="255" y1="64.2809" x2="255" y2="446.6263">
<stop offset="0" style="stop-color:#4A7DEE"/>
<stop offset="1" style="stop-color:#D6E7FD"/>
</linearGradient>
<circle style="fill:url(#SVGID_00000069371419516710993290000014674188358113625238_);" cx="255" cy="255.5" r="191.2"/>
<linearGradient id="SVGID_00000065064387537909975690000008168498501573575072_" gradientUnits="userSpaceOnUse" x1="255" y1="78.3427" x2="255" y2="432.5645">
<stop offset="0" style="stop-color:#1052FA"/>
<stop offset="1" style="stop-color:#03A2FF"/>
</linearGradient>
<circle style="fill:url(#SVGID_00000065064387537909975690000008168498501573575072_);" cx="255" cy="255.5" r="177.1"/>
<radialGradient id="SVGID_00000033342088705839663170000005065284211989201043_" cx="255" cy="259.6645" r="177.5771" gradientTransform="matrix(1 0 0 1.0099 0 -2.5659)" gradientUnits="userSpaceOnUse">
<stop offset="0.2827" style="stop-color:#000000;stop-opacity:0"/>
<stop offset="0.94" style="stop-color:#000000;stop-opacity:0"/>
<stop offset="1" style="stop-color:#00001E;stop-opacity:0.6"/>
</radialGradient>
<circle style="fill:url(#SVGID_00000033342088705839663170000005065284211989201043_);" cx="255" cy="255.5" r="177.1"/>
<radialGradient id="SVGID_00000035526984743727634550000013253980538971136412_" cx="254.9778" cy="255.7856" r="139.7306" gradientTransform="matrix(0.9782 0 0 0.9837 5.5688 6.0985)" gradientUnits="userSpaceOnUse">
<stop offset="0.96" style="stop-color:#000000;stop-opacity:0.4"/>
<stop offset="1" style="stop-color:#000000;stop-opacity:0"/>
</radialGradient>
<ellipse style="fill:url(#SVGID_00000035526984743727634550000013253980538971136412_);" cx="255" cy="257.7" rx="136.7" ry="137.5"/>
<g>
<linearGradient id="SVGID_00000150796304906679271230000000770736055720491697_" gradientUnits="userSpaceOnUse" x1="255" y1="120.2667" x2="255" y2="390.6405">
<stop offset="0" style="stop-color:#FBF6F6"/>
<stop offset="1" style="stop-color:#D4DFF6"/>
</linearGradient>
<circle style="fill:url(#SVGID_00000150796304906679271230000000770736055720491697_);" cx="255" cy="255.5" r="135.2"/>
</g>
<radialGradient id="SVGID_00000019660750539865065670000003955002333269141426_" cx="255.3853" cy="299.8408" r="111.8273" fx="255.3853" fy="318.9533" gradientUnits="userSpaceOnUse">
<stop offset="0.3659" style="stop-color:#E6E9FA"/>
<stop offset="0.5491" style="stop-color:#E3E7F9"/>
<stop offset="0.6816" style="stop-color:#DADFF4"/>
<stop offset="0.7983" style="stop-color:#CAD2EC"/>
<stop offset="0.905" style="stop-color:#B4C0E1"/>
<stop offset="1" style="stop-color:#99AAD4"/>
</radialGradient>
<linearGradient id="SVGID_00000030477569789705423270000000820441070347519890_" gradientUnits="userSpaceOnUse" x1="255" y1="323.7591" x2="255" y2="187.1481">
<stop offset="0" style="stop-color:#F2F2F2"/>
<stop offset="1" style="stop-color:#9BA6CE"/>
</linearGradient>
<circle style="fill:url(#SVGID_00000019660750539865065670000003955002333269141426_);stroke:url(#SVGID_00000030477569789705423270000000820441070347519890_);stroke-miterlimit:10;" cx="255" cy="255.5" r="67.8"/>
<g>
<linearGradient id="SVGID_00000060022293084610081460000017109631303786632858_" gradientUnits="userSpaceOnUse" x1="255.0003" y1="323.8614" x2="255.0003" y2="187.0463">
<stop offset="0" style="stop-color:#E6E9FA"/>
<stop offset="1" style="stop-color:#99AAD4"/>
</linearGradient>
<path style="fill:url(#SVGID_00000060022293084610081460000017109631303786632858_);" d="M255,323.9c-37.7,0-68.4-30.7-68.4-68.4
c0-37.7,30.7-68.4,68.4-68.4c37.7,0,68.4,30.7,68.4,68.4C323.4,293.2,292.7,323.9,255,323.9z M255,188.2
c-37.1,0-67.2,30.1-67.2,67.2s30.1,67.2,67.2,67.2s67.2-30.1,67.2-67.2S292.1,188.2,255,188.2z"/>
</g>
<linearGradient id="SVGID_00000043443407870630449420000003935611237313607865_" gradientUnits="userSpaceOnUse" x1="255.2881" y1="350.8774" x2="255.2881" y2="159.9432">
<stop offset="0" style="stop-color:#191D37"/>
<stop offset="1" style="stop-color:#0A0D1B"/>
</linearGradient>
<path style="fill:url(#SVGID_00000043443407870630449420000003935611237313607865_);" d="M272.5,350.9h-34.4c-7.2,0-13-5.9-13-13
V246c0-1.2,0.6-2.4,1.6-3.2l10.1-7.4c2.1-1.5,2.1-4.6,0.1-6.2l-10.4-8.2c-0.9-0.7-1.5-1.9-1.5-3.1v-45c0-7.2,5.9-13,13-13h34.4
c7.2,0,13,5.9,13,13v92c0,1.2-0.5,2.3-1.5,3.1l-10.6,8.3c-2,1.6-2,4.5-0.1,6.1l10.7,8.7c0.9,0.7,1.4,1.9,1.4,3v43.5
C285.5,345,279.6,350.9,272.5,350.9z"/>
<linearGradient id="SVGID_00000124847186351877336530000004144667077095366310_" gradientUnits="userSpaceOnUse" x1="15.6569" y1="255.4536" x2="15.6569" y2="249.7905">
<stop offset="0" style="stop-color:#000000;stop-opacity:0.4"/>
<stop offset="0.933" style="stop-color:#000000;stop-opacity:0"/>
</linearGradient>
<path style="fill:url(#SVGID_00000124847186351877336530000004144667077095366310_);" d="M0,250.6v4.9h31.3c0-1.6,0-3.3,0.1-4.9H0z
"/>
<linearGradient id="SVGID_00000096743138351438000510000017129655145029114014_" gradientUnits="userSpaceOnUse" x1="15.6569" y1="260.3536" x2="15.6569" y2="254.6487">
<stop offset="5.326722e-02" style="stop-color:#FFFFFF;stop-opacity:0"/>
<stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0.5"/>
</linearGradient>
<path style="fill:url(#SVGID_00000096743138351438000510000017129655145029114014_);" d="M31.3,255.5H0v4.9h31.3
C31.3,258.7,31.3,257.1,31.3,255.5z"/>
<linearGradient id="SVGID_00000071543710920805015200000000421061880900545470_" gradientUnits="userSpaceOnUse" x1="-159.9804" y1="255.4536" x2="-159.9804" y2="249.7905" gradientTransform="matrix(-1 0 0 1 335.3627 0)">
<stop offset="0" style="stop-color:#000000;stop-opacity:0.4"/>
<stop offset="0.933" style="stop-color:#000000;stop-opacity:0"/>
</linearGradient>
<path style="fill:url(#SVGID_00000071543710920805015200000000421061880900545470_);" d="M512,250.6v4.9h-33.3c0-1.6,0-3.3-0.1-4.9
H512z"/>
<linearGradient id="SVGID_00000109007114612485663690000009752698984711783319_" gradientUnits="userSpaceOnUse" x1="-159.9804" y1="260.3536" x2="-159.9804" y2="254.6487" gradientTransform="matrix(-1 0 0 1 335.3627 0)">
<stop offset="5.326722e-02" style="stop-color:#FFFFFF;stop-opacity:0"/>
<stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0.5"/>
</linearGradient>
<path style="fill:url(#SVGID_00000109007114612485663690000009752698984711783319_);" d="M478.7,255.5H512v4.9h-33.3
C478.7,258.7,478.7,257.1,478.7,255.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -65,6 +65,8 @@
<EmbeddedResource Include="TestData\Exports\bitwarden.csv" />
<None Remove="TestData\Exports\keepass.kdbx.csv" />
<EmbeddedResource Include="TestData\Exports\keepass.kdbx.csv" />
<None Remove="TestData\Exports\1password_8.csv" />
<EmbeddedResource Include="TestData\Exports\1password_8.csv" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,5 @@
Title,Url,Username,Password,OTPAuth,Favorite,Archived,Tags,Notes
Test record 2 with 2FA,,username2fa,password2fa,otpauth://totp/Strongbox?secret=PLW4SB3PQ7MKVXY2MXF4NEXS6Y&period=30&algorithm=SHA1&digits=6,false,false,,Notes about 2FA record
1Password Account (dpatel),https://my.1password.com,derekpatel@aliasvault.net,passwordexample,,false,false,Starter Kit,You can use this login to sign in to your account on 1password.com.
Test record 1,https://nu.nl,username1,password1,,false,false,,
Password entry,,passwordusername,passwordpassword,,false,false,,
1 Title Url Username Password OTPAuth Favorite Archived Tags Notes
2 Test record 2 with 2FA username2fa password2fa otpauth://totp/Strongbox?secret=PLW4SB3PQ7MKVXY2MXF4NEXS6Y&period=30&algorithm=SHA1&digits=6 false false Notes about 2FA record
3 1Password Account (dpatel) https://my.1password.com derekpatel@aliasvault.net passwordexample false false Starter Kit You can use this login to sign in to your account on 1password.com.
4 Test record 1 https://nu.nl username1 password1 false false
5 Password entry passwordusername passwordpassword false false

View File

@@ -20,6 +20,7 @@ public class ImportExportTests
/// <summary>
/// Test case for importing credentials from CSV and ensuring all values are present.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task ImportCredentialsFromCsv()
{
@@ -79,16 +80,15 @@ public class ImportExportTests
Assert.That(importedCredential.ServiceUrl, Is.EqualTo(credential.Service.Url));
Assert.That(importedCredential.Username, Is.EqualTo(credential.Username));
Assert.That(importedCredential.Notes, Is.EqualTo(credential.Notes));
Assert.That(importedCredential.CreatedAt, Is.EqualTo(credential.CreatedAt));
Assert.That(importedCredential.UpdatedAt, Is.EqualTo(credential.UpdatedAt));
Assert.That(importedCredential.CreatedAt?.Date, Is.EqualTo(credential.CreatedAt.Date));
Assert.That(importedCredential.UpdatedAt?.Date, Is.EqualTo(credential.UpdatedAt.Date));
Assert.That(importedCredential.Alias!.Gender, Is.EqualTo(credential.Alias!.Gender));
Assert.That(importedCredential.Alias!.FirstName, Is.EqualTo(credential.Alias!.FirstName));
Assert.That(importedCredential.Alias!.LastName, Is.EqualTo(credential.Alias!.LastName));
Assert.That(importedCredential.Alias!.NickName, Is.EqualTo(credential.Alias!.NickName));
Assert.That(importedCredential.Alias!.BirthDate, Is.EqualTo(credential.Alias!.BirthDate));
Assert.That(importedCredential.Alias!.Email, Is.EqualTo(credential.Alias!.Email));
Assert.That(importedCredential.Alias!.CreatedAt, Is.EqualTo(credential.Alias!.CreatedAt));
Assert.That(importedCredential.Alias!.UpdatedAt, Is.EqualTo(credential.Alias!.UpdatedAt));
Assert.That(importedCredential.Alias!.CreatedAt?.Date, Is.EqualTo(credential.Alias!.CreatedAt.Date));
Assert.That(importedCredential.Alias!.UpdatedAt?.Date, Is.EqualTo(credential.Alias!.UpdatedAt.Date));
Assert.That(importedCredential.Password, Is.EqualTo(credential.Passwords.First().Value));
});
}
@@ -165,4 +165,42 @@ public class ImportExportTests
Assert.That(sampleCredential.Password, Is.EqualTo("&3V_$z?Aiw-_x+nbYj"));
});
}
/// <summary>
/// Test case for importing credentials from 1Password CSV and ensuring all values are present.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task ImportCredentialsFrom1PasswordCsv()
{
// Arrange
var fileContent = await ResourceReaderUtility.ReadEmbeddedResourceStringAsync("AliasVault.UnitTests.TestData.Exports.1password_8.csv");
// Act
var importedCredentials = await OnePasswordImporter.ImportFromCsvAsync(fileContent);
// Assert
Assert.That(importedCredentials, Has.Count.EqualTo(4));
// Test specific entries
var twoFactorCredential = importedCredentials.First(c => c.Username == "username2fa");
Assert.Multiple(() =>
{
Assert.That(twoFactorCredential.ServiceName, Is.EqualTo("Test record 2 with 2FA"));
Assert.That(twoFactorCredential.Username, Is.EqualTo("username2fa"));
Assert.That(twoFactorCredential.Password, Is.EqualTo("password2fa"));
Assert.That(twoFactorCredential.TwoFactorSecret, Is.EqualTo("otpauth://totp/Strongbox?secret=PLW4SB3PQ7MKVXY2MXF4NEXS6Y&period=30&algorithm=SHA1&digits=6"));
Assert.That(twoFactorCredential.Notes, Is.EqualTo("Notes about 2FA record"));
});
var onePasswordAccount = importedCredentials.First(c => c.ServiceName == "1Password Account (dpatel)");
Assert.Multiple(() =>
{
Assert.That(onePasswordAccount.ServiceName, Is.EqualTo("1Password Account (dpatel)"));
Assert.That(onePasswordAccount.ServiceUrl, Is.EqualTo("https://my.1password.com"));
Assert.That(onePasswordAccount.Username, Is.EqualTo("derekpatel@aliasvault.net"));
Assert.That(onePasswordAccount.Password, Is.EqualTo("passwordexample"));
Assert.That(onePasswordAccount.Notes, Is.EqualTo("You can use this login to sign in to your account on 1password.com."));
});
}
}

View File

@@ -0,0 +1,54 @@
//-----------------------------------------------------------------------
// <copyright file="BitwardenImporter.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 1Password.
/// </summary>
public class OnePasswordImporter
{
/// <summary>
/// Imports 1Password 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<OnePasswordCsvRecord>())
{
var credential = new ImportedCredential
{
ServiceName = record.Title,
ServiceUrl = record.Url,
Username = record.Username,
Password = record.Password,
TwoFactorSecret = record.OTPAuth,
Notes = record.Notes
};
credentials.Add(credential);
}
if (credentials.Count == 0)
{
throw new InvalidOperationException("No records found in the CSV file.");
}
return credentials;
}
}

View File

@@ -0,0 +1,73 @@
//-----------------------------------------------------------------------
// <copyright file="BitwardenCsvRecord.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 1Password CSV record that is being imported from a 1Password CSV export file.
/// </summary>
public class OnePasswordCsvRecord
{
/// <summary>
/// Gets or sets the title of the item.
/// </summary>
[Name("Title")]
public string Title { 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 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 the OTP (One-Time Password) authentication secret.
/// </summary>
[Name("OTPAuth")]
public string? OTPAuth { get; set; }
/// <summary>
/// Gets or sets whether the item is favorited.
/// </summary>
[Name("Favorite")]
[TypeConverter(typeof(BooleanConverter))]
public bool Favorite { get; set; }
/// <summary>
/// Gets or sets whether the item is archived.
/// </summary>
[Name("Archived")]
[TypeConverter(typeof(BooleanConverter))]
public bool Archived { get; set; }
/// <summary>
/// Gets or sets the service URL.
/// </summary>
[Name("Tags")]
public string? Tags { get; set; }
/// <summary>
/// Gets or sets any additional notes.
/// </summary>
[Name("Notes")]
public string? Notes { get; set; }
}