mirror of
https://github.com/BeyondDimension/SteamTools.git
synced 2026-10-26 03:59:34 +08:00
ExportAuthToQRCode Format & Brotli
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
+22
-9
@@ -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; }
|
||||
|
||||
+115
-35
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
-1
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user