From 2a9bba79daedd0b0f1fca5e732981ff5e2e3c05e Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Wed, 18 Mar 2026 10:03:57 +0100 Subject: [PATCH] Add TOTP urldecode on import (#773) --- .../Utilities/ImportExportTests.cs | 12 ++++++++++- .../AliasVault.TotpGenerator/TotpHelper.cs | 20 +++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/apps/server/Tests/AliasVault.UnitTests/Utilities/ImportExportTests.cs b/apps/server/Tests/AliasVault.UnitTests/Utilities/ImportExportTests.cs index f39a025f8..1dd548a19 100644 --- a/apps/server/Tests/AliasVault.UnitTests/Utilities/ImportExportTests.cs +++ b/apps/server/Tests/AliasVault.UnitTests/Utilities/ImportExportTests.cs @@ -117,7 +117,7 @@ public class ImportExportTests Assert.That(aliasVaultCredential.Password, Is.EqualTo("toor")); }); - // Test entry with multiple URLs (TutaNota3) + // Test entry with multiple URLs (TutaNota3) and URL-encoded TOTP URI var multiUrlCredential = importedCredentials.First(c => c.ServiceName == "TutaNota3"); Assert.Multiple(() => { @@ -127,6 +127,7 @@ public class ImportExportTests Assert.That(multiUrlCredential.ServiceUrls[1], Is.EqualTo("https://app.aliasvault.net")); Assert.That(multiUrlCredential.ServiceUrls[2], Is.EqualTo("https://downloads.aliasvault.net")); Assert.That(multiUrlCredential.Username, Is.EqualTo("avtest3@tutamail.com")); + Assert.That(multiUrlCredential.TwoFactorSecret, Is.EqualTo("otpauth://totp/Test%20name%3Atest%40test.org?secret=PLW4SB3PQ7MKVXY2MXF4NEXS6Y&issuer=Alias%20Vault")); }); // Verify multiple URLs get converted to multiple FieldValues @@ -142,6 +143,15 @@ public class ImportExportTests Assert.That(urlFieldValues[1].Weight, Is.EqualTo(1)); Assert.That(urlFieldValues[2].Weight, Is.EqualTo(2)); }); + + // Verify TOTP code name is properly URL-decoded when converting from otpauth URI + var multiUrlItemTotpCode = multiUrlItem.TotpCodes.FirstOrDefault(); + Assert.That(multiUrlItemTotpCode, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(multiUrlItemTotpCode!.SecretKey, Is.EqualTo("PLW4SB3PQ7MKVXY2MXF4NEXS6Y")); + Assert.That(multiUrlItemTotpCode.Name, Is.EqualTo("Alias Vault: Test name:test@test.org"), "TOTP name should be URL-decoded from the otpauth URI"); + }); } /// diff --git a/apps/server/Utilities/AliasVault.TotpGenerator/TotpHelper.cs b/apps/server/Utilities/AliasVault.TotpGenerator/TotpHelper.cs index ab5f1116c..b0a02b877 100644 --- a/apps/server/Utilities/AliasVault.TotpGenerator/TotpHelper.cs +++ b/apps/server/Utilities/AliasVault.TotpGenerator/TotpHelper.cs @@ -35,13 +35,25 @@ public static class TotpHelper if (string.IsNullOrWhiteSpace(name)) { // The label is everything after 'totp/' and before '?' - var label = uri.AbsolutePath.TrimStart('/'); + var label = System.Web.HttpUtility.UrlDecode(uri.AbsolutePath.TrimStart('/')); - // If the label contains ':', take the part after it - name = label.Contains(':') ? label.Split(':')[1] : label; + // Check if there's an issuer in the query params + var issuer = queryParams["issuer"]; + + // If the label contains ':', it might be in the format "issuer:account" + // Only split if there's no issuer in query params (to avoid splitting account names with colons) + if (label.Contains(':') && string.IsNullOrWhiteSpace(issuer)) + { + // Split on the first colon only + var colonIndex = label.IndexOf(':'); + name = label.Substring(colonIndex + 1); + } + else + { + name = label; + } // If there's an issuer in the query params, use it as a prefix - var issuer = queryParams["issuer"]; if (!string.IsNullOrWhiteSpace(issuer)) { name = $"{issuer}: {name}";