From 8feb787b7f4142ef07c80edfe96af97d960d07a4 Mon Sep 17 00:00:00 2001 From: Aigio Liu Date: Mon, 26 Jul 2021 01:31:24 +0800 Subject: [PATCH] ExportAuthToQRCode Format & Brotli --- .../Extensions/ByteArrayExtensions.cs | 91 ++++++++++- .../Extensions/StringExtensions.cs | 44 +++-- .../Models/MyAuthenticator.cs | 4 - .../Services/Mvvm/AuthService.cs | 75 ++++++--- .../AddAuthWindowViewModel.shared.cs | 31 ++-- .../ExportAuthWindowViewModel.shared.cs | 150 ++++++++++++++---- .../main/res/layout/activity_detail_auth.xml | 58 +++++++ .../main/res/layout/activity_export_auth.xml | 1 - .../LocalAuthPage/AuthDetailActivity.cs | 11 +- .../LocalAuthPage/ExportAuthActivity.cs | 28 +++- .../LocalAuthPage/SteamAuthDataActivity.cs | 3 +- .../LocalAuthPage/SteamAuthTradeActivity.cs | 3 +- .../LocalAuthSteamToolsImportFragment.cs | 2 +- src/ST.Client/UI/QRCodeHelper.QRCoder.cs | 56 +++++++ src/ST.Client/UI/QRCodeHelper.cs | 19 +-- .../Values/Base/GAPAuthenticatorValueDTO.cs | 2 +- .../WinAuth/IGAPAuthenticatorDTOExtensions.cs | 81 ++++++++++ .../ST.Tools.OpenSourceLibraryList.csproj | 3 + 18 files changed, 540 insertions(+), 122 deletions(-) create mode 100644 src/ST.Client/UI/QRCodeHelper.QRCoder.cs create mode 100644 src/ST.Services.CloudService/WinAuth/IGAPAuthenticatorDTOExtensions.cs diff --git a/src/Common.CoreLib/Extensions/ByteArrayExtensions.cs b/src/Common.CoreLib/Extensions/ByteArrayExtensions.cs index a43e3016d..616dbc855 100644 --- a/src/Common.CoreLib/Extensions/ByteArrayExtensions.cs +++ b/src/Common.CoreLib/Extensions/ByteArrayExtensions.cs @@ -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 buffer) => MemoryMarshal.Cast(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; + } } } \ No newline at end of file diff --git a/src/Common.CoreLib/Extensions/StringExtensions.cs b/src/Common.CoreLib/Extensions/StringExtensions.cs index 5fac82d88..2d536b221 100644 --- a/src/Common.CoreLib/Extensions/StringExtensions.cs +++ b/src/Common.CoreLib/Extensions/StringExtensions.cs @@ -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); + /// + [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); + } + /// + [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); } diff --git a/src/ST.Client.Desktop/Models/MyAuthenticator.cs b/src/ST.Client.Desktop/Models/MyAuthenticator.cs index 5671e0bda..85f956eec 100644 --- a/src/ST.Client.Desktop/Models/MyAuthenticator.cs +++ b/src/ST.Client.Desktop/Models/MyAuthenticator.cs @@ -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) { - } } diff --git a/src/ST.Client.Desktop/Services/Mvvm/AuthService.cs b/src/ST.Client.Desktop/Services/Mvvm/AuthService.cs index 1ff4067c5..14f5dd5b3 100644 --- a/src/ST.Client.Desktop/Services/Mvvm/AuthService.cs +++ b/src/ST.Client.Desktop/Services/Mvvm/AuthService.cs @@ -28,13 +28,13 @@ namespace System.Application.Services private readonly GAPRepository repository = DI.Get(); - public SourceCache Authenticators { get; } + public SourceCache Authenticators { get; } public GAPRepository Repository => repository; public AuthService() { - Authenticators = new SourceCache(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(); } - /// - /// WinAuth令牌导入 - /// - public void ImportWinAuthenticators(string file, bool isLocal, string? password) + /// + public void ImportWinAuthenticators(IEnumerable 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 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; + } + } + + /// + /// WinAuth令牌导入 + /// + public void ImportWinAuthenticators(string filePath, bool isLocal, string? password) + { + var urls = ReadUrlsByFilePath(filePath); + ImportWinAuthenticators(urls, isLocal, password); + } + /// /// Steam APP令牌导入 /// @@ -676,13 +693,21 @@ namespace System.Application.Services Toast.Show(AppResources.LocalAuth_ProtectionAuth_Success); } - public async Task GetExportAuthenticatorsAsync(bool isLocal, string? password = null) + public IEnumerable GetExportSourceAuthenticators(Func? 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 GetExportAuthenticatorsAsync(bool isLocal, string? password = null, Func? 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? 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); } diff --git a/src/ST.Client.Desktop/UI/ViewModels/Windows/LocalAuthPage/AddAuthWindowViewModel.shared.cs b/src/ST.Client.Desktop/UI/ViewModels/Windows/LocalAuthPage/AddAuthWindowViewModel.shared.cs index daadaa46c..56707d1ac 100644 --- a/src/ST.Client.Desktop/UI/ViewModels/Windows/LocalAuthPage/AddAuthWindowViewModel.shared.cs +++ b/src/ST.Client.Desktop/UI/ViewModels/Windows/LocalAuthPage/AddAuthWindowViewModel.shared.cs @@ -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(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; } diff --git a/src/ST.Client.Desktop/UI/ViewModels/Windows/LocalAuthPage/ExportAuthWindowViewModel.shared.cs b/src/ST.Client.Desktop/UI/ViewModels/Windows/LocalAuthPage/ExportAuthWindowViewModel.shared.cs index 4a5725883..cd8ff2fde 100644 --- a/src/ST.Client.Desktop/UI/ViewModels/Windows/LocalAuthPage/ExportAuthWindowViewModel.shared.cs +++ b/src/ST.Client.Desktop/UI/ViewModels/Windows/LocalAuthPage/ExportAuthWindowViewModel.shared.cs @@ -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; + } + } + + /// + /// 选中Id,当此值有效时,仅导出此值对应的令牌 + /// + public ushort SelectId { get; set; } + + /// + /// 当前选中令牌 + /// + MyAuthenticator? SelectAuthenticator + { + get + { + if (SelectId != default) + { + var vm = AuthService.Current.Authenticators.Items.FirstOrDefault(x => x.Id == SelectId); + return vm; + } + return null; + } + } + + /// + /// 当前选中令牌唯一显示名称 + /// + 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 func) + async Task ExportAuthCore(Func 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? 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; + /// + /// 默认导出文件名 + /// + 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) { diff --git a/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/layout/activity_detail_auth.xml b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/layout/activity_detail_auth.xml index e9a257667..7a6a54686 100644 --- a/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/layout/activity_detail_auth.xml +++ b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/layout/activity_detail_auth.xml @@ -320,6 +320,64 @@ + + + + + + + + + + + + + + + x.Id == this.GetViewModel()); + var vm = AuthService.Current.Authenticators.Items.FirstOrDefault(x => x.Id == this.GetViewModel()); 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) diff --git a/src/ST.Client.Mobile.Droid/UI/Activities/LocalAuthPage/ExportAuthActivity.cs b/src/ST.Client.Mobile.Droid/UI/Activities/LocalAuthPage/ExportAuthActivity.cs index d25b758ff..fcc794db9 100644 --- a/src/ST.Client.Mobile.Droid/UI/Activities/LocalAuthPage/ExportAuthActivity.cs +++ b/src/ST.Client.Mobile.Droid/UI/Activities/LocalAuthPage/ExportAuthActivity.cs @@ -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(); + 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(activity, authId); + } } } \ No newline at end of file diff --git a/src/ST.Client.Mobile.Droid/UI/Activities/LocalAuthPage/SteamAuthDataActivity.cs b/src/ST.Client.Mobile.Droid/UI/Activities/LocalAuthPage/SteamAuthDataActivity.cs index e846a6354..3cb883547 100644 --- a/src/ST.Client.Mobile.Droid/UI/Activities/LocalAuthPage/SteamAuthDataActivity.cs +++ b/src/ST.Client.Mobile.Droid/UI/Activities/LocalAuthPage/SteamAuthDataActivity.cs @@ -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()); + var vm = AuthService.Current.Authenticators.Items.FirstOrDefault(x => x.Id == this.GetViewModel()); return new(vm); } diff --git a/src/ST.Client.Mobile.Droid/UI/Activities/LocalAuthPage/SteamAuthTradeActivity.cs b/src/ST.Client.Mobile.Droid/UI/Activities/LocalAuthPage/SteamAuthTradeActivity.cs index d9ba584dc..fa63fa7b9 100644 --- a/src/ST.Client.Mobile.Droid/UI/Activities/LocalAuthPage/SteamAuthTradeActivity.cs +++ b/src/ST.Client.Mobile.Droid/UI/Activities/LocalAuthPage/SteamAuthTradeActivity.cs @@ -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()); + var vm = AuthService.Current.Authenticators.Items.FirstOrDefault(x => x.Id == this.GetViewModel()); return new(vm); } diff --git a/src/ST.Client.Mobile.Droid/UI/Fragments/LocalAuthPage/LocalAuthSteamToolsImportFragment.cs b/src/ST.Client.Mobile.Droid/UI/Fragments/LocalAuthPage/LocalAuthSteamToolsImportFragment.cs index f6dd5a5b1..1160e391d 100644 --- a/src/ST.Client.Mobile.Droid/UI/Fragments/LocalAuthPage/LocalAuthSteamToolsImportFragment.cs +++ b/src/ST.Client.Mobile.Droid/UI/Fragments/LocalAuthPage/LocalAuthSteamToolsImportFragment.cs @@ -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; } diff --git a/src/ST.Client/UI/QRCodeHelper.QRCoder.cs b/src/ST.Client/UI/QRCodeHelper.QRCoder.cs new file mode 100644 index 000000000..ac048d6e9 --- /dev/null +++ b/src/ST.Client/UI/QRCodeHelper.QRCoder.cs @@ -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); + } + } + } +} \ No newline at end of file diff --git a/src/ST.Client/UI/QRCodeHelper.cs b/src/ST.Client/UI/QRCodeHelper.cs index eef3a4e1b..3e6f834bd 100644 --- a/src/ST.Client/UI/QRCodeHelper.cs +++ b/src/ST.Client/UI/QRCodeHelper.cs @@ -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) + /// + /// QRCode生成结果 + /// + 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, } } } \ No newline at end of file diff --git a/src/ST.Services.CloudService/Models/GAPAuthenticators/Values/Base/GAPAuthenticatorValueDTO.cs b/src/ST.Services.CloudService/Models/GAPAuthenticators/Values/Base/GAPAuthenticatorValueDTO.cs index e7f0f5c65..929790448 100644 --- a/src/ST.Services.CloudService/Models/GAPAuthenticators/Values/Base/GAPAuthenticatorValueDTO.cs +++ b/src/ST.Services.CloudService/Models/GAPAuthenticators/Values/Base/GAPAuthenticatorValueDTO.cs @@ -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; diff --git a/src/ST.Services.CloudService/WinAuth/IGAPAuthenticatorDTOExtensions.cs b/src/ST.Services.CloudService/WinAuth/IGAPAuthenticatorDTOExtensions.cs new file mode 100644 index 000000000..adf08429c --- /dev/null +++ b/src/ST.Services.CloudService/WinAuth/IGAPAuthenticatorDTOExtensions.cs @@ -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 + { + /// + /// Create a KeyUriFormat compatible URL + /// See https://code.google.com/p/google-authenticator/wiki/KeyUriFormat + /// + /// + /// + /// + 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())); + + 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; + } + } +} \ No newline at end of file diff --git a/src/ST.Tools.OpenSourceLibraryList/ST.Tools.OpenSourceLibraryList.csproj b/src/ST.Tools.OpenSourceLibraryList/ST.Tools.OpenSourceLibraryList.csproj index 5d50d90e0..014881130 100644 --- a/src/ST.Tools.OpenSourceLibraryList/ST.Tools.OpenSourceLibraryList.csproj +++ b/src/ST.Tools.OpenSourceLibraryList/ST.Tools.OpenSourceLibraryList.csproj @@ -27,6 +27,9 @@ Extensions\StringExtensions.cs + + Extensions\ByteArrayExtensions.cs + ProjectPathUtil.cs