ExportAuthToQRCode Format & Brotli

This commit is contained in:
Aigio Liu
2021-07-26 01:31:33 +08:00
parent 21fe71edff
commit 8feb787b7f
18 changed files with 540 additions and 122 deletions
@@ -1,4 +1,7 @@
using System.Runtime.InteropServices;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Compression;
using System.Runtime.InteropServices;
// ReSharper disable once CheckNamespace
namespace System
@@ -22,5 +25,91 @@ namespace System
public static byte[] ToByteArray(this ReadOnlySpan<sbyte> buffer)
=> MemoryMarshal.Cast<sbyte, byte>(buffer).ToArray();
[return: NotNullIfNotNull("buffer")]
public static byte[]? CompressByteArray(this byte[]? buffer)
{
if (buffer == null) return null;
using var memoryStream = new MemoryStream();
using (var gZipStream = new GZipStream(memoryStream, CompressionMode.Compress, true))
{
gZipStream.Write(buffer, 0, buffer.Length);
}
memoryStream.Position = 0;
var compressedData = new byte[memoryStream.Length];
memoryStream.Read(compressedData, 0, compressedData.Length);
var gZipBuffer = new byte[compressedData.Length + 4];
Buffer.BlockCopy(compressedData, 0, gZipBuffer, 4, compressedData.Length);
Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, gZipBuffer, 0, 4);
return gZipBuffer;
}
[return: NotNullIfNotNull("buffer")]
public static byte[]? CompressByteArrayByBrotli(this byte[]? buffer)
{
if (buffer == null) return null;
using var memoryStream = new MemoryStream();
using (var gZipStream = new BrotliStream(memoryStream, CompressionMode.Compress, true))
{
gZipStream.Write(buffer, 0, buffer.Length);
}
memoryStream.Position = 0;
var compressedData = new byte[memoryStream.Length];
memoryStream.Read(compressedData, 0, compressedData.Length);
var gZipBuffer = new byte[compressedData.Length + 4];
Buffer.BlockCopy(compressedData, 0, gZipBuffer, 4, compressedData.Length);
Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, gZipBuffer, 0, 4);
return gZipBuffer;
}
[return: NotNullIfNotNull("gZipBuffer")]
public static byte[]? DecompressByteArray(this byte[]? gZipBuffer)
{
if (gZipBuffer == null) return null;
using var memoryStream = new MemoryStream();
int dataLength = BitConverter.ToInt32(gZipBuffer, 0);
memoryStream.Write(gZipBuffer, 4, gZipBuffer.Length - 4);
var buffer = new byte[dataLength];
memoryStream.Position = 0;
using (var gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
{
gZipStream.Read(buffer, 0, buffer.Length);
}
return buffer;
}
[return: NotNullIfNotNull("gZipBuffer")]
public static byte[]? DecompressByteArrayByBrotli(this byte[]? gZipBuffer)
{
if (gZipBuffer == null) return null;
using var memoryStream = new MemoryStream();
int dataLength = BitConverter.ToInt32(gZipBuffer, 0);
memoryStream.Write(gZipBuffer, 4, gZipBuffer.Length - 4);
var buffer = new byte[dataLength];
memoryStream.Position = 0;
using (var gZipStream = new BrotliStream(memoryStream, CompressionMode.Decompress))
{
gZipStream.Read(buffer, 0, buffer.Length);
}
return buffer;
}
}
}
@@ -231,20 +231,17 @@ namespace System
{
if (string.IsNullOrEmpty(text)) return null;
byte[] buffer = Encoding.UTF8.GetBytes(text);
var memoryStream = new MemoryStream();
using (var gZipStream = new GZipStream(memoryStream, CompressionMode.Compress, true))
{
gZipStream.Write(buffer, 0, buffer.Length);
}
var gZipBuffer = buffer.CompressByteArray();
return Convert.ToBase64String(gZipBuffer);
}
memoryStream.Position = 0;
var compressedData = new byte[memoryStream.Length];
memoryStream.Read(compressedData, 0, compressedData.Length);
var gZipBuffer = new byte[compressedData.Length + 4];
Buffer.BlockCopy(compressedData, 0, gZipBuffer, 4, compressedData.Length);
Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, gZipBuffer, 0, 4);
/// <inheritdoc cref="CompressString(string?)"/>
[return: NotNullIfNotNull("text")]
public static string? CompressStringByBrotli(this string? text)
{
if (string.IsNullOrEmpty(text)) return null;
byte[] buffer = Encoding.UTF8.GetBytes(text);
var gZipBuffer = buffer.CompressByteArrayByBrotli();
return Convert.ToBase64String(gZipBuffer);
}
@@ -258,18 +255,17 @@ namespace System
{
if (string.IsNullOrEmpty(compressedText)) return null;
byte[] gZipBuffer = Convert.FromBase64String(compressedText);
using var memoryStream = new MemoryStream();
int dataLength = BitConverter.ToInt32(gZipBuffer, 0);
memoryStream.Write(gZipBuffer, 4, gZipBuffer.Length - 4);
var buffer = new byte[dataLength];
memoryStream.Position = 0;
using (var gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
{
gZipStream.Read(buffer, 0, buffer.Length);
}
var buffer = gZipBuffer.DecompressByteArray();
return Encoding.UTF8.GetString(buffer);
}
/// <inheritdoc cref="DecompressString(string?)"/>
[return: NotNullIfNotNull("compressedText")]
public static string? DecompressStringByBrotli(this string? compressedText)
{
if (string.IsNullOrEmpty(compressedText)) return null;
byte[] gZipBuffer = Convert.FromBase64String(compressedText);
var buffer = gZipBuffer.DecompressByteArrayByBrotli();
return Encoding.UTF8.GetString(buffer);
}
@@ -9,7 +9,6 @@ using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using WinAuth;
using static System.Application.Models.GAPAuthenticatorValueDTO;
namespace System.Application.Models
{
@@ -79,7 +78,6 @@ namespace System.Application.Models
}
}
public int Index
{
get
@@ -300,7 +298,6 @@ namespace System.Application.Models
AuthenticatorData.Value.ServerTimeDiff = reader.ReadElementContentAsLong();
break;
default:
reader.Skip();
break;
@@ -383,7 +380,6 @@ namespace System.Application.Models
}
catch (OperationCanceledException)
{
}
}
@@ -28,13 +28,13 @@ namespace System.Application.Services
private readonly GAPRepository repository = DI.Get<GAPRepository>();
public SourceCache<MyAuthenticator, int> Authenticators { get; }
public SourceCache<MyAuthenticator, ushort> Authenticators { get; }
public GAPRepository Repository => repository;
public AuthService()
{
Authenticators = new SourceCache<MyAuthenticator, int>(t => t.Id);
Authenticators = new(t => t.Id);
}
public async Task InitializeAsync(bool isSync = false)
@@ -138,29 +138,17 @@ namespace System.Application.Services
return auths.Count();
}
/// <summary>
/// WinAuth令牌导入
/// </summary>
public void ImportWinAuthenticators(string file, bool isLocal, string? password)
/// <inheritdoc cref="ImportAuthenticatorFile(string, bool, string?, string?)"/>
public void ImportWinAuthenticators(IEnumerable<string> urls, bool isLocal, string? password)
{
StringBuilder lines = new StringBuilder();
bool retry;
do
{
retry = false;
lines.Length = 0;
// read a plain text file
lines.Append(File.ReadAllText(file));
} while (retry);
int linenumber = 0;
try
{
using var sr = new StringReader(lines.ToString());
string? line;
while ((line = sr.ReadLine()) != null)
foreach (var url in urls)
{
linenumber++;
line = url;
// ignore blank lines or comments
line = line.Trim();
@@ -318,16 +306,45 @@ namespace System.Application.Services
}
Toast.Show(AppResources.LocalAuth_AddAuthSuccess);
}
catch (UriFormatException ex)
catch (UriFormatException)
{
throw new UriFormatException(string.Format("Invalid authenticator at line {0}", linenumber), ex);
Toast.Show(string.Format("UriFormatException Invalid authenticator at line {0}", linenumber));
}
catch (Exception ex)
{
throw new Exception(string.Format("Error importing at line {0}", ex.Message), ex);
Toast.Show(string.Format("Error importing at line {0}", ex.Message));
}
}
static IEnumerable<string> ReadUrlsByFilePath(string filePath)
{
StringBuilder lines = new();
bool retry;
do
{
retry = false;
lines.Length = 0;
// read a plain text file
lines.Append(File.ReadAllText(filePath));
} while (retry);
using var sr = new StringReader(lines.ToString());
string? line;
while ((line = sr.ReadLine()) != null)
{
yield return line;
}
}
/// <summary>
/// WinAuth令牌导入
/// </summary>
public void ImportWinAuthenticators(string filePath, bool isLocal, string? password)
{
var urls = ReadUrlsByFilePath(filePath);
ImportWinAuthenticators(urls, isLocal, password);
}
/// <summary>
/// Steam APP令牌导入
/// </summary>
@@ -676,13 +693,21 @@ namespace System.Application.Services
Toast.Show(AppResources.LocalAuth_ProtectionAuth_Success);
}
public async Task<byte[]> GetExportAuthenticatorsAsync(bool isLocal, string? password = null)
public IEnumerable<IGAPAuthenticatorDTO> GetExportSourceAuthenticators(Func<MyAuthenticator, bool>? predicateWhere = null)
{
var bt = await repository.ExportAsync(isLocal, password, Authenticators.Items.Select(s => s.AuthenticatorData));
var items = Authenticators.Items;
if (predicateWhere != null) items = items.Where(predicateWhere);
return items.Select(s => s.AuthenticatorData);
}
public async Task<byte[]> GetExportAuthenticatorsAsync(bool isLocal, string? password = null, Func<MyAuthenticator, bool>? predicateWhere = null)
{
var items = GetExportSourceAuthenticators(predicateWhere);
var bt = await repository.ExportAsync(isLocal, password, items);
return bt;
}
public async void ExportAuthenticators(string? filePath, bool isLocal, string? password = null)
public async void ExportAuthenticators(string? filePath, bool isLocal, string? password = null, Func<MyAuthenticator, bool>? predicateWhere = null)
{
try
{
@@ -694,7 +719,7 @@ namespace System.Application.Services
IOPath.FileIfExistsItDelete(filePath);
var bt = await GetExportAuthenticatorsAsync(isLocal, password);
var bt = await GetExportAuthenticatorsAsync(isLocal, password, predicateWhere);
await File.WriteAllBytesAsync(filePath, bt);
}
@@ -12,6 +12,7 @@ using System.Windows;
using System.Windows.Input;
using Xamarin.Essentials;
using static System.Application.FilePicker2;
using static System.Application.Repositories.IGameAccountPlatformAuthenticatorRepository;
namespace System.Application.UI.ViewModels
{
@@ -395,29 +396,41 @@ namespace System.Application.UI.ViewModels
});
}
public void ImportWinAuth(string file)
public void ImportWinAuth(string filePath)
{
AuthService.Current.ImportWinAuthenticators(file, AuthIsLocal, AuthPassword);
AuthService.Current.ImportWinAuthenticators(filePath, AuthIsLocal, AuthPassword);
}
public void ImportSDA(string file)
public void ImportSDA(string filePath)
{
AuthService.Current.ImportSDAFile(file, AuthIsLocal, AuthPassword);
AuthService.Current.ImportSDAFile(filePath, AuthIsLocal, AuthPassword);
}
public void ImportSteamPlusPlusV1(string file)
public void ImportSteamPlusPlusV1(string filePath)
{
AuthService.Current.ImportSteamToolsV1Authenticator(file, AuthIsLocal, AuthPassword);
AuthService.Current.ImportSteamToolsV1Authenticator(filePath, AuthIsLocal, AuthPassword);
}
public void ImportSteamPlusPlusV2(string file)
public void ImportSteamPlusPlusV2(string filePath)
{
AuthService.Current.ImportAuthenticatorFile(file, AuthIsLocal, AuthPassword);
AuthService.Current.ImportAuthenticatorFile(filePath, AuthIsLocal, AuthPassword);
}
public void ImportSteamPlusPlusV2(byte[] bytes)
{
AuthService.Current.ImportAuthenticatorFile(bytes, AuthIsLocal, AuthPassword);
try
{
var bytes_decompress_br = bytes.DecompressByteArrayByBrotli();
var urls = Serializable.DMP<string[]>(bytes_decompress_br);
if (urls != null)
{
AuthService.Current.ImportWinAuthenticators(urls, AuthIsLocal, AuthPassword);
}
}
catch
{
Toast.Show(string.Format(AppResources.LocalAuth_ExportAuth_Error, ImportResultCode.IncorrectFormat));
}
}
public ICommand SppV2Btn_Click { get; }
@@ -1,26 +1,78 @@
using QRCoder.Exceptions;
using ReactiveUI;
using System.Application.Models;
using System.Application.Services;
using System.Application.UI.Resx;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Properties;
using System.Threading.Tasks;
using static System.Application.FilePicker2;
using WinAuth;
namespace System.Application.UI.ViewModels
{
public partial class ExportAuthWindowViewModel
{
public static string TitleName => AppResources.LocalAuth_ExportAuth;
public ExportAuthWindowViewModel() : base()
public new string Title
{
Title =
get
{
var title =
#if !__MOBILE__
ThisAssembly.AssemblyTrademark + " | " +
#endif
TitleName;
AppResources.LocalAuth_ExportAuth;
var mark = SelectAuthenticatorMark;
if (mark != default)
{
title = $"{title}({mark})";
}
return title;
}
}
/// <summary>
/// 选中Id,当此值有效时,仅导出此值对应的令牌
/// </summary>
public ushort SelectId { get; set; }
/// <summary>
/// 当前选中令牌
/// </summary>
MyAuthenticator? SelectAuthenticator
{
get
{
if (SelectId != default)
{
var vm = AuthService.Current.Authenticators.Items.FirstOrDefault(x => x.Id == SelectId);
return vm;
}
return null;
}
}
/// <summary>
/// 当前选中令牌唯一显示名称
/// </summary>
string? SelectAuthenticatorMark
{
get
{
var authenticator = SelectAuthenticator;
if (authenticator != default)
{
return (string.IsNullOrEmpty(authenticator.Name) ? authenticator.Id.ToString() : authenticator.Name);
}
return null;
}
}
public ExportAuthWindowViewModel() : base()
{
base.Title = Title;
#if !__MOBILE__
SelectPathButton_Click = ReactiveCommand.CreateFromTask(async () =>
@@ -110,7 +162,7 @@ namespace System.Application.UI.ViewModels
set => this.RaiseAndSetIfChanged(ref _IsExporting, value);
}
async Task ExportAuthCore(Func<bool, string?, Task> func)
async Task ExportAuthCore(Func<Task> func, bool ignorePath = false, bool ignorePassword = false)
{
var (success, _) = await AuthService.Current.HasPasswordEncryptionShowPassWordWindow();
if (!success)
@@ -118,26 +170,29 @@ namespace System.Application.UI.ViewModels
this.Close();
}
if (string.IsNullOrEmpty(Path))
if (!ignorePath && string.IsNullOrEmpty(Path))
{
Toast.Show(AppResources.LocalAuth_ProtectionAuth_PathError);
return;
}
if (IsPasswordEncrypt)
if (!ignorePassword)
{
if (string.IsNullOrWhiteSpace(VerifyPassword) && VerifyPassword != Password)
if (IsPasswordEncrypt)
{
Toast.Show(AppResources.LocalAuth_ProtectionAuth_PasswordErrorTip);
return;
if (string.IsNullOrWhiteSpace(VerifyPassword) && VerifyPassword != Password)
{
Toast.Show(AppResources.LocalAuth_ProtectionAuth_PasswordErrorTip);
return;
}
}
else
{
VerifyPassword = null;
}
}
else
{
VerifyPassword = null;
}
await func(IsOnlyCurrentComputerEncrypt, VerifyPassword);
await func();
}
public async void ExportAuth()
@@ -156,9 +211,18 @@ namespace System.Application.UI.ViewModels
IsExporting = false;
}
async Task ExportAuthToFileAsync() => await ExportAuthCore((isLocal, password) =>
Func<MyAuthenticator, bool>? Filter
{
AuthService.Current.ExportAuthenticators(Path, isLocal, password);
get
{
if (SelectId == default) return null;
return x => x.Id == SelectId;
}
}
async Task ExportAuthToFileAsync() => await ExportAuthCore(() =>
{
AuthService.Current.ExportAuthenticators(Path, IsOnlyCurrentComputerEncrypt, VerifyPassword, Filter);
this.Close();
@@ -167,30 +231,46 @@ namespace System.Application.UI.ViewModels
return Task.CompletedTask;
});
async Task ExportAuthToQRCodeAsync() => await ExportAuthCore(async (isLocal, password) =>
async Task ExportAuthToQRCodeAsync() => await ExportAuthCore(async () =>
{
var bytes = await AuthService.Current.GetExportAuthenticatorsAsync(isLocal, password);
QRCode = await Task.Run(() => CreateQRCode(bytes));
});
var datas = AuthService.Current.GetExportSourceAuthenticators(Filter);
QRCode = await Task.Run(() =>
{
var urls = datas.Select(x => x.ToUrl()).ToArray();
var bytes = Serializable.SMP(urls);
//var bytes_compress_gzip = bytes.CompressByteArray();
var bytes_compress_br = bytes.CompressByteArrayByBrotli();
return CreateQRCode(bytes_compress_br);
});
}, ignorePath: true, ignorePassword: true);
static Stream? CreateQRCode(byte[] bytes)
{
try
(var result, var stream, var e) = QRCodeHelper.Create(bytes);
switch (result)
{
return QRCodeHelper.Create(bytes);
case QRCodeHelper.QRCodeCreateResult.DataTooLong:
Toast.Show(AppResources.AuthLocal_ExportToQRCodeTooLongErrorTip);
break;
case QRCodeHelper.QRCodeCreateResult.Exception:
Toast.Show(e!.ToString());
break;
}
catch (DataTooLongException)
{
Toast.Show(AppResources.AuthLocal_ExportToQRCodeTooLongErrorTip);
}
catch (Exception e)
{
Toast.Show(e.ToString());
}
return null;
return stream;
}
public static string DefaultExportAuthFileName => "Steam++ Authenticator " + DateTime.Now.ToString(DateTimeFormat.Date) + FileEx.MPO;
/// <summary>
/// 默认导出文件名
/// </summary>
public string DefaultExportAuthFileName
{
get
{
var mark = SelectAuthenticatorMark;
var markIsNull = mark == default;
return $"Steam++ Authenticator{(markIsNull ? "s" : default)} {(markIsNull ? default : $"({mark}) ")}{DateTime.Now.ToString(DateTimeFormat.Date)}{FileEx.MPO}";
}
}
protected override void Dispose(bool disposing)
{
@@ -320,6 +320,64 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
<View
android:layout_width="match_parent"
android:layout_height="@dimen/border_width"
android:background="@color/grey" />
<FrameLayout
android:id="@+id/layoutExport"
android:layout_width="match_parent"
android:layout_height="@dimen/preference_button_height_large"
android:background="?android:windowBackground"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/ivIconExport"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="@dimen/activity_horizontal_margin"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/baseline_save_alt_black_24"
app:tint="@color/icon_background"
xamarin:managedType="AndroidX.AppCompat.Widget.AppCompatImageView" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tvTitleExport"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:textColor="@color/text_primary"
android:textSize="@dimen/preference_button_font_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/ivIconExport"
app:layout_constraintTop_toTopOf="parent"
tools:text="导出令牌"
xamarin:managedType="AndroidX.AppCompat.Widget.AppCompatTextView" />
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="@dimen/gap_space_min"
android:background="@android:color/transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/outline_chevron_right_black_24"
app:tint="@color/grey"
xamarin:managedType="AndroidX.AppCompat.Widget.AppCompatImageView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
<View
android:layout_width="match_parent"
android:layout_height="@dimen/border_width"
@@ -30,7 +30,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/abc_action_button_min_height_material"
xamarin:managedType="AndroidX.AppCompat.Widget.AppCompatImageView" />
<LinearLayout
@@ -6,6 +6,7 @@ using Android.Views;
using Binding;
using ReactiveUI;
using System.Application.Models;
using System.Application.Services;
using System.Application.UI.Resx;
using System.Application.UI.ViewModels;
using System.Linq;
@@ -29,7 +30,7 @@ namespace System.Application.UI.Activities
protected override MyAuthenticatorWrapper? OnCreateViewModel()
{
var vm = LocalAuthPageViewModel.Current.Authenticators?.FirstOrDefault(x => x.Id == this.GetViewModel<ushort>());
var vm = AuthService.Current.Authenticators.Items.FirstOrDefault(x => x.Id == this.GetViewModel<ushort>());
return vm!;
}
@@ -73,13 +74,15 @@ namespace System.Application.UI.Activities
binding.tvTitleConfirmTrade.Text = LocalAuth_AuthTrade;
binding.tvTitleSeeData.Text = SeeData;
binding.tvTitleDelete.Text = LocalAuth_Delete;
binding.tvTitleExport.Text = LocalAuth_ExportAuth;
}).AddTo(this);
SetOnClickListener(binding!.layoutValue,
binding.layoutEditName,
binding.layoutConfirmTrade,
binding.layoutSeeData,
binding.layoutDelete);
binding.layoutDelete,
binding.layoutExport);
}
protected override void OnPause()
@@ -123,6 +126,10 @@ namespace System.Application.UI.Activities
Finish();
});
}
else if (view.Id == Resource.Id.layoutExport)
{
ExportAuthActivity.StartActivity(this, ViewModel!.Authenticator.Id);
}
}
public static void StartActivity(Activity activity, ushort authId)
@@ -21,6 +21,12 @@ namespace System.Application.UI.Activities
{
protected override int? LayoutResource => Resource.Layout.activity_export_auth;
protected override ExportAuthWindowViewModel? OnCreateViewModel()
{
var selectId = this.GetViewModel<ushort>();
return new() { SelectId = selectId };
}
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
@@ -42,7 +48,7 @@ namespace System.Application.UI.Activities
R.Current.WhenAnyValue(x => x.Res).Subscribe(_ =>
{
Title = TitleName;
Title = ViewModel!.Title;
if (binding == null) return;
binding.tvExportFilePathLabel.Text = LocalAuth_ExportAuth_ExportPath + ":";
binding.swExportEncryption.Text = LocalAuth_ExportAuth_EncryptionExport;
@@ -56,8 +62,8 @@ namespace System.Application.UI.Activities
ViewModel!.WhenAnyValue(x => x.IsPasswordEncrypt).Subscribe(value =>
{
if (binding == null) return;
binding.layoutPassword.Enabled = value;
binding.layoutPassword2.Enabled = value;
binding.layoutPassword.Enabled = value && !ViewModel!.IsExportQRCode;
binding.layoutPassword2.Enabled = value && !ViewModel!.IsExportQRCode;
}).AddTo(this);
ViewModel!.WhenAnyValue(x => x.Path).Subscribe(value =>
{
@@ -67,12 +73,17 @@ namespace System.Application.UI.Activities
ViewModel!.WhenAnyValue(x => x.IsExportQRCode).Subscribe(value =>
{
if (binding == null) return;
binding.layoutExportFilePath.Visibility = value ? ViewStates.Invisible : ViewStates.Visible;
binding.layoutExportFilePath.Visibility = value ? (ViewModel!.QRCode == null ? ViewStates.Invisible : ViewStates.Gone) : ViewStates.Visible;
binding.swExportEncryption.Enabled = !value;
binding.layoutPassword.Enabled = ViewModel!.IsPasswordEncrypt && !value;
binding.layoutPassword2.Enabled = ViewModel!.IsPasswordEncrypt && !value;
}).AddTo(this);
ViewModel!.WhenAnyValue(x => x.QRCode).Subscribe(value =>
{
if (binding == null) return;
binding.ivQRCode.Visibility = value == null ? ViewStates.Gone : ViewStates.Visible;
var hasValue = value != null;
if (hasValue) binding.layoutExportFilePath.Visibility = ViewStates.Gone;
binding.ivQRCode.Visibility = !hasValue ? ViewStates.Gone : ViewStates.Visible;
binding.ivQRCode.SetImageSource(value);
}).AddTo(this);
ViewModel!.WhenAnyValue(x => x.IsExporting).Subscribe(value =>
@@ -100,7 +111,7 @@ namespace System.Application.UI.Activities
SetOnClickListener(binding.btnExport);
ViewModel!.Path = Path.Combine(externalPath, DefaultExportAuthDirName, DefaultExportAuthFileName);
ViewModel!.Path = Path.Combine(externalPath, DefaultExportAuthDirName, ViewModel!.DefaultExportAuthFileName);
}
protected override void OnClick(View view)
@@ -112,5 +123,10 @@ namespace System.Application.UI.Activities
}
base.OnClick(view);
}
public static void StartActivity(Activity activity, ushort authId)
{
GoToPlatformPages.StartActivity<ExportAuthActivity, ushort>(activity, authId);
}
}
}
@@ -4,6 +4,7 @@ using Android.OS;
using Android.Runtime;
using Binding;
using ReactiveUI;
using System.Application.Services;
using System.Application.UI.Resx;
using System.Application.UI.ViewModels;
using System.Linq;
@@ -21,7 +22,7 @@ namespace System.Application.UI.Activities
protected override ShowAuthWindowViewModel? OnCreateViewModel()
{
var vm = LocalAuthPageViewModel.Current.Authenticators?.FirstOrDefault(x => x.Id == this.GetViewModel<ushort>());
var vm = AuthService.Current.Authenticators.Items.FirstOrDefault(x => x.Id == this.GetViewModel<ushort>());
return new(vm);
}
@@ -8,6 +8,7 @@ using AndroidX.RecyclerView.Widget;
using AndroidX.SwipeRefreshLayout.Widget;
using Binding;
using ReactiveUI;
using System.Application.Services;
using System.Application.UI.Adapters;
using System.Application.UI.Resx;
using System.Application.UI.ViewModels;
@@ -29,7 +30,7 @@ namespace System.Application.UI.Activities
protected override AuthTradeWindowViewModel? OnCreateViewModel()
{
var vm = LocalAuthPageViewModel.Current.Authenticators?.FirstOrDefault(x => x.Id == this.GetViewModel<ushort>());
var vm = AuthService.Current.Authenticators.Items.FirstOrDefault(x => x.Id == this.GetViewModel<ushort>());
return new(vm);
}
@@ -51,7 +51,7 @@ namespace System.Application.UI.Fragments
scanner.StartScan(x =>
{
if (!x.RawBytes.Any_Nullable()) return;
ViewModel!.ImportSteamPlusPlusV2(x.RawBytes);
ViewModel!.ImportSteamPlusPlusV2(x.RawBytes!);
});
return true;
}
+56
View File
@@ -0,0 +1,56 @@
using QRCoder;
using QRCoder.Exceptions;
using System.IO;
using static QRCoder.QRCodeGenerator;
namespace System.Application.UI
{
public static partial class QRCodeHelper
{
const int DefaultPixelsPerModule = 6;
static Stream GetStream(this QRCodeData qrCodeData, int pixelsPerModule = DefaultPixelsPerModule)
{
using BitmapByteQRCode qrCode = new(qrCodeData);
var qrCodeBytes = qrCode.GetGraphic(pixelsPerModule);
MemoryStream ms = new(qrCodeBytes);
return ms;
}
public static (QRCodeCreateResult result, Stream? stream, Exception? exception) Create(byte[] bytes, ECCLevel level = ECCLevel.L, int pixelsPerModule = DefaultPixelsPerModule)
{
try
{
var qrCodeData = GenerateQrCode(bytes, level);
var ms = qrCodeData.GetStream(pixelsPerModule);
return (QRCodeCreateResult.Success, ms, null);
}
catch (DataTooLongException e)
{
return (QRCodeCreateResult.DataTooLong, null, e);
}
catch (Exception e)
{
return (QRCodeCreateResult.Exception, null, e);
}
}
public static (QRCodeCreateResult result, Stream? stream, Exception? exception) Create(string str, ECCLevel level = ECCLevel.L, int pixelsPerModule = DefaultPixelsPerModule)
{
try
{
var qrCodeData = GenerateQrCode(str, level);
var ms = qrCodeData.GetStream(pixelsPerModule);
return (QRCodeCreateResult.Success, ms, null);
}
catch (DataTooLongException e)
{
return (QRCodeCreateResult.DataTooLong, null, e);
}
catch (Exception e)
{
return (QRCodeCreateResult.Exception, null, e);
}
}
}
}
+8 -11
View File
@@ -1,18 +1,15 @@
using QRCoder;
using System.IO;
using static QRCoder.QRCodeGenerator;
namespace System.Application.UI
{
public static class QRCodeHelper
public static partial class QRCodeHelper
{
public static Stream Create(byte[] bytes, ECCLevel level = ECCLevel.L, int pixelsPerModule = 4)
/// <summary>
/// QRCode生成结果
/// </summary>
public enum QRCodeCreateResult
{
var qrCodeData = GenerateQrCode(bytes, level);
using BitmapByteQRCode qrCode = new(qrCodeData);
var qrCodeBytes = qrCode.GetGraphic(pixelsPerModule);
MemoryStream ms = new(qrCodeBytes);
return ms;
Success,
DataTooLong,
Exception,
}
}
}
@@ -1,4 +1,4 @@
using MPIgnore = MessagePack.IgnoreMemberAttribute;
using MPIgnore = MessagePack.IgnoreMemberAttribute;
using N_JsonIgnore = Newtonsoft.Json.JsonIgnoreAttribute;
using S_JsonIgnore = System.Text.Json.Serialization.JsonIgnoreAttribute;
@@ -0,0 +1,81 @@
using System;
using System.Application.Models;
using System.Text.RegularExpressions;
using System.Web;
using static System.Application.Models.GAPAuthenticatorValueDTO;
using Base32 = WinAuth.WinAuthBase32;
// ReSharper disable once CheckNamespace
namespace WinAuth
{
public static class GAPAuthenticatorDTOExtensions
{
/// <summary>
/// Create a KeyUriFormat compatible URL
/// See https://code.google.com/p/google-authenticator/wiki/KeyUriFormat
/// </summary>
/// <param name="this"></param>
/// <param name="compat"></param>
/// <returns></returns>
public static string ToUrl(this IGAPAuthenticatorDTO @this, bool compat = false)
{
string type = "totp";
string extraparams = string.Empty;
Match match;
var issuer = @this.Value.Issuer;
var label = @this.Name;
if (string.IsNullOrEmpty(issuer) == true && (match = Regex.Match(label, @"^([^\(]+)\s+\((.*?)\)(.*)")).Success == true)
{
issuer = match.Groups[1].Value;
label = match.Groups[2].Value + match.Groups[3].Value;
}
if (string.IsNullOrEmpty(issuer) == false && (match = Regex.Match(label, @"^" + issuer + @"\s+\((.*?)\)(.*)")).Success == true)
{
label = match.Groups[1].Value + match.Groups[2].Value;
}
if (string.IsNullOrEmpty(issuer) == false)
{
extraparams += "&issuer=" + HttpUtility.UrlEncode(issuer);
}
if (@this.Value.HMACType != DEFAULT_HMAC_TYPE)
{
extraparams += "&algorithm=" + @this.Value.HMACType.ToString();
}
if (@this.Value is BattleNetAuthenticator battleNetAuthenticator)
{
extraparams += "&serial=" + HttpUtility.UrlEncode(battleNetAuthenticator.Serial?.Replace("-", ""));
}
else if (@this.Value is SteamAuthenticator steamAuthenticator)
{
if (!compat)
{
extraparams += "&deviceid=" + HttpUtility.UrlEncode(steamAuthenticator.DeviceId);
extraparams += "&data=" + HttpUtility.UrlEncode(steamAuthenticator.SteamData);
}
}
else if (@this.Value is HOTPAuthenticator hOTPAuthenticator)
{
type = "hotp";
extraparams += "&counter=" + hOTPAuthenticator.Counter;
}
string secret = HttpUtility.UrlEncode(Base32.GetInstance().Encode(@this.Value.SecretKey ?? Array.Empty<byte>()));
if (@this.Value.Period != DEFAULT_PERIOD)
{
extraparams += "&period=" + @this.Value.Period;
}
var url = string.Format("otpauth://" + type + "/{0}?secret={1}&digits={2}{3}",
(string.IsNullOrEmpty(issuer) == false ? HttpUtility.UrlPathEncode(issuer) + ":" + HttpUtility.UrlPathEncode(label) : HttpUtility.UrlPathEncode(label)),
secret,
@this.Value.CodeDigits,
extraparams);
return url;
}
}
}
@@ -27,6 +27,9 @@
<Compile Include="..\Common.CoreLib\Extensions\StringExtensions.cs">
<Link>Extensions\StringExtensions.cs</Link>
</Compile>
<Compile Include="..\Common.CoreLib\Extensions\ByteArrayExtensions.cs">
<Link>Extensions\ByteArrayExtensions.cs</Link>
</Compile>
<Compile Include="..\..\resources\ProjectPathUtil.cs">
<Link>ProjectPathUtil.cs</Link>
</Compile>