Skip to content

Commit

Permalink
Add export as Authenticator Pro backup
Browse files Browse the repository at this point in the history
  • Loading branch information
timokoessler committed Mar 29, 2024
1 parent c902fb4 commit 60ed278
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 28 deletions.
142 changes: 142 additions & 0 deletions Guard/Core/Export/Exporter/AuthenticatorProExporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using System.IO;
using System.Text;
using System.Text.Json;
using Guard.Core.Models;
using Guard.Core.Security;
using NSec.Cryptography;

namespace Guard.Core.Export.Exporter
{
internal class AuthenticatorProExporter : IExporter
{
public string Name => "AuthenticatorPro";

public IExporter.ExportType Type => IExporter.ExportType.File;
public string ExportFileExtensions => "Authenticator Pro Backup (*.authpro) | *.authpro";

public bool RequiresPassword() => true;

public string GetDefaultFileName()
{
return $"2FAGuard-AuthenticatorPro-{DateTime.Now:yyyy-MM-dd-HH-mm-ss}.authpro";
}

private const string StrongHeader = "AUTHENTICATORPRO";

private const int ArgonParallelism = 4;
private const int ArgonIterations = 3;
private const int ArgonMemorySize = 65536;

private const int SaltLength = 16;
private const int KeyLength = 32;
private const int IvLength = 12;

public async Task Export(string? path, byte[]? password)
{
ArgumentNullException.ThrowIfNull(path);
ArgumentNullException.ThrowIfNull(password);

var tokenHelpers =
await TokenManager.GetAllTokens()
?? throw new Exception(I18n.GetString("export.notokens"));
List<AuthenticatorProBackup.Authenticator> authenticators = [];

foreach (var tokenHelper in tokenHelpers)
{
AuthenticatorProBackup.Authenticator authenticator =
new()
{
Issuer = tokenHelper.dBToken.Issuer,
Username = tokenHelper.Username,
Secret = tokenHelper.DecryptedSecret,
Digits = tokenHelper.dBToken.Digits ?? 6,
Period = tokenHelper.dBToken.Period ?? 30,
Type = AuthenticatorProBackup.AuthenticatorType.Totp,
CopyCount = 0,
Counter = 0,
Ranking = 0,
Pin = null,
Icon = null,
};

if (tokenHelper.dBToken.Algorithm != null)
{
authenticator.Algorithm = tokenHelper.dBToken.Algorithm switch
{
TOTPAlgorithm.SHA1 => AuthenticatorProBackup.HashAlgorithm.Sha1,
TOTPAlgorithm.SHA256 => AuthenticatorProBackup.HashAlgorithm.Sha256,
TOTPAlgorithm.SHA512 => AuthenticatorProBackup.HashAlgorithm.Sha512,
_
=> throw new Exception(
$"Invalid algorithm {tokenHelper.dBToken.Algorithm}"
),
};
}
else
{
authenticator.Algorithm = AuthenticatorProBackup.HashAlgorithm.Sha1;
}

authenticators.Add(authenticator);
}

AuthenticatorProBackup authProBackup =
new()
{
Authenticators = [.. authenticators],
Categories = [],
AuthenticatorCategories = [],
CustomIcons = []
};

byte[] data = JsonSerializer.SerializeToUtf8Bytes(authProBackup);
byte[] salt = EncryptionHelper.GetRandomBytes(SaltLength);
byte[] nonce = EncryptionHelper.GetRandomBytes(IvLength);

var argon2id = new Konscious.Security.Cryptography.Argon2id(password)
{
DegreeOfParallelism = ArgonParallelism,
Iterations = ArgonIterations,
MemorySize = ArgonMemorySize,
Salt = salt
};

byte[] keyBytes = argon2id.GetBytes(KeyLength);

if (!Aes256Gcm.IsSupported)
{
throw new Exception(
"This platform does not support hardware-accelerated AES (GCM) encryption that is required to import this file."
);
}

Aes256Gcm aes = new();
Key key = Key.Import(aes, keyBytes, KeyBlobFormat.RawSymmetricKey);
byte[] encrypted = aes.Encrypt(key, nonce, null, data);

byte[] strongHeaderBytes = Encoding.UTF8.GetBytes(StrongHeader);

byte[] result = new byte[
strongHeaderBytes.Length + salt.Length + nonce.Length + encrypted.Length
];
Buffer.BlockCopy(strongHeaderBytes, 0, result, 0, strongHeaderBytes.Length);
Buffer.BlockCopy(salt, 0, result, strongHeaderBytes.Length, salt.Length);
Buffer.BlockCopy(
nonce,
0,
result,
strongHeaderBytes.Length + salt.Length,
nonce.Length
);
Buffer.BlockCopy(
encrypted,
0,
result,
strongHeaderBytes.Length + salt.Length + nonce.Length,
encrypted.Length
);

await File.WriteAllBytesAsync(path, result);
}
}
}
10 changes: 5 additions & 5 deletions Guard/Core/Import/Importer/AuthenticatorProImporter.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using Guard.Core.Icons;
using System.IO;
using System.Text;
using System.Text.Json;
using Guard.Core.Icons;
using Guard.Core.Models;
using Guard.Core.Security;
using NSec.Cryptography;
using System.IO;
using System.Text;
using System.Text.Json;

namespace Guard.Core.Import.Importer
{
Expand Down Expand Up @@ -133,7 +133,7 @@ private enum BackupType
AuthenticatorProBackup.HashAlgorithm.Sha256 => TOTPAlgorithm.SHA256,
AuthenticatorProBackup.HashAlgorithm.Sha512 => TOTPAlgorithm.SHA512,
_
=> throw new Exception(
=> throw new Exception(
$"Invalid AuthenticatorPro: Unsupported algorithm {token.Algorithm}"
),
};
Expand Down
6 changes: 5 additions & 1 deletion Guard/Core/Models/AuthenticatorProBackup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ public class Authenticator
public int? CopyCount { get; set; }
public int? Ranking { get; set; }
public string? Icon { get; set; }
public string? Pin { get; set; }
}

public Authenticator[]? Authenticators { get; set; }
public IEnumerable<Authenticator>? Authenticators { get; set; }
public IEnumerable<object>? Categories { get; set; }
public IEnumerable<object>? AuthenticatorCategories { get; set; }
public IEnumerable<object>? CustomIcons { get; set; }
}
}
20 changes: 4 additions & 16 deletions Guard/Core/Security/EncryptionHelper.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using NSec.Cryptography;
using System.Security.Cryptography;
using System.Security.Cryptography;
using System.Text;
using NSec.Cryptography;

namespace Guard.Core.Security
{
Expand Down Expand Up @@ -80,24 +80,12 @@ public static string GenerateSalt()

public static byte[] GenerateSaltBytes()
{
byte[] salt;
using (var rng = RandomNumberGenerator.Create())
{
salt = new byte[SaltSize];
rng.GetBytes(salt);
}
return salt;
return GetRandomBytes(SaltSize);
}

private static byte[] GenerateNonce()
{
byte[] nonce;
using (var rng = RandomNumberGenerator.Create())
{
nonce = new byte[NonceSize];
rng.GetBytes(nonce);
}
return nonce;
return GetRandomBytes(NonceSize);
}

private static byte[] DeriveKey(byte[] pass, byte[] salt)
Expand Down
7 changes: 5 additions & 2 deletions Guard/Core/Storage/Reset.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Guard.Core.Installation;
using System.IO;
using System.IO;
using Guard.Core.Installation;
using Guard.Core.Security;

namespace Guard.Core.Storage
{
Expand All @@ -9,6 +10,8 @@ internal static void DeleteEverything()
{
string path = InstallationInfo.GetAppDataFolderPath();
Database.Deinit();
_ = WindowsHello.Unregister();
Auth.DeleteWindowsHelloProtectedKey();

string[] files = ["auth-keys", "settings", "TokenDatabase.db", "TokenDatabase-log.db"];

Expand Down
2 changes: 2 additions & 0 deletions Guard/Resources/Strings.de.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@
<system:String x:Key="i.export.success.content">Die Tokens wurden erfolgreich exportiert.</system:String>
<system:String x:Key="i.export.success.open">Ordner öffnen</system:String>
<system:String x:Key="i.export.notokens">Es sind keine Tokens vorhanden, die exportiert werden können</system:String>
<system:String x:Key="i.export.authpro">Authenticator Pro</system:String>
<system:String x:Key="i.export.authpro.description">Exportiere die Tokens als Authenticator Pro Backup</system:String>

<!-- Token Details -->
<system:String x:Key="i.td.general">Allgemeine Informationen</system:String>
Expand Down
2 changes: 2 additions & 0 deletions Guard/Resources/Strings.en.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@
<system:String x:Key="i.export.success.content">Your tokens have been exported successfully.</system:String>
<system:String x:Key="i.export.success.open">Open folder</system:String>
<system:String x:Key="i.export.notokens">There are no tokens that can be exported</system:String>
<system:String x:Key="i.export.authpro">Authenticator Pro</system:String>
<system:String x:Key="i.export.authpro.description">Export tokens as a Authenticator Pro backup file</system:String>

<!-- Token Details -->
<system:String x:Key="i.td.general">General information</system:String>
Expand Down
26 changes: 26 additions & 0 deletions Guard/Views/Pages/ExportPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,32 @@
TextWrapping="WrapWithOverflow" />
</StackPanel>
</ui:CardAction>
<ui:CardAction
Width="350"
Height="95"
Margin="0,15,15,0"
Click="AuthenticatorPro_Click">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<svgc:SvgViewbox
Grid.Column="0"
Height="32"
Source="pack://application:,,,/Assets/authenticatorpro.svg" />
<StackPanel Grid.Column="1" Margin="15,0,8,0">
<ui:TextBlock
FontTypography="BodyStrong"
Text="{DynamicResource i.export.authpro}"
TextWrapping="WrapWithOverflow" />
<ui:TextBlock
Appearance="Secondary"
Text="{DynamicResource i.export.authpro.description}"
TextWrapping="WrapWithOverflow" />
</StackPanel>
</Grid>
</ui:CardAction>
</WrapPanel>
</StackPanel>
</Page>
13 changes: 9 additions & 4 deletions Guard/Views/Pages/ExportPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using Guard.Core;
using Guard.Core.Export.Exporter;
using Guard.Views.Controls;
using System.Diagnostics;
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using Guard.Core;
using Guard.Core.Export.Exporter;
using Guard.Views.Controls;
using Wpf.Ui.Controls;

namespace Guard.Views.Pages
Expand Down Expand Up @@ -117,5 +117,10 @@ private void UriList_Click(object sender, RoutedEventArgs e)
{
Export(new UriListExporter());
}

private void AuthenticatorPro_Click(object sender, RoutedEventArgs e)
{
Export(new AuthenticatorProExporter());
}
}
}

0 comments on commit 60ed278

Please sign in to comment.