diff --git a/src/ST.Client.Desktop/Services/Mvvm/AuthService.cs b/src/ST.Client.Desktop/Services/Mvvm/AuthService.cs index 14f5dd5b3c9..6749f267118 100644 --- a/src/ST.Client.Desktop/Services/Mvvm/AuthService.cs +++ b/src/ST.Client.Desktop/Services/Mvvm/AuthService.cs @@ -162,7 +162,7 @@ public void ImportWinAuthenticators(IEnumerable urls, bool isLocal, stri var qm = line.IndexOf("?"); if (hash != -1 && hash < qm) { - line = $"{line.Substring(0, hash)}%23{line.Substring(hash + 1)}"; + line = $"{line.Substring(0, hash)}%23{line[(hash + 1)..]}"; } // parse and validate URI @@ -181,25 +181,25 @@ public void ImportWinAuthenticators(IEnumerable urls, bool isLocal, stri // get the label and optional issuer string issuer = string.Empty; - string label = (string.IsNullOrEmpty(uri.LocalPath) == false ? uri.LocalPath.Substring(1) : string.Empty); // skip past initial / + string label = string.IsNullOrEmpty(uri.LocalPath) == false ? uri.LocalPath[1..] : string.Empty; // skip past initial / int p = label.IndexOf(":"); if (p != -1) { issuer = label.Substring(0, p); - label = label.Substring(p + 1); + label = label[(p + 1)..]; } // + aren't decoded label = label.Replace("+", " "); var query = HttpUtility.ParseQueryString(uri.Query); string secret = query["secret"]; - if (string.IsNullOrEmpty(secret) == true) + if (string.IsNullOrEmpty(secret)) { throw new ApplicationException("Authenticator does not contain secret"); } string counter = query["counter"]; - if (uri.Host == "hotp" && string.IsNullOrEmpty(counter) == true) + if (uri.Host == "hotp" && string.IsNullOrEmpty(counter)) { throw new ApplicationException("HOTP authenticator should have a counter"); } @@ -210,7 +210,7 @@ public void ImportWinAuthenticators(IEnumerable urls, bool isLocal, stri if (string.Compare(issuer, "BattleNet", true) == 0) { string serial = query["serial"]; - if (string.IsNullOrEmpty(serial) == true) + if (string.IsNullOrEmpty(serial)) { throw new ApplicationException("Battle.net Authenticator does not have a serial"); } @@ -245,7 +245,7 @@ public void ImportWinAuthenticators(IEnumerable urls, bool isLocal, stri ((HOTPAuthenticator)auth).SecretKey = WinAuthBase32.GetInstance().Decode(secret); ((HOTPAuthenticator)auth).Counter = int.Parse(counter); - if (string.IsNullOrEmpty(issuer) == false) + if (!string.IsNullOrEmpty(issuer)) { auth.Issuer = issuer; } @@ -259,7 +259,7 @@ public void ImportWinAuthenticators(IEnumerable urls, bool isLocal, stri { issuer = string.Empty; } - else if (string.IsNullOrEmpty(issuer) == false) + else if (!string.IsNullOrEmpty(issuer)) { auth.Issuer = issuer; } 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 56707d1acec..ee2a46d5409 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 @@ -6,12 +6,14 @@ using System.Collections.Generic; using System.IO; using System.Net; +using System.Linq; using System.Properties; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; using Xamarin.Essentials; using static System.Application.FilePicker2; +using static System.Application.Models.GAPAuthenticatorDTO; using static System.Application.Repositories.IGameAccountPlatformAuthenticatorRepository; namespace System.Application.UI.ViewModels @@ -421,9 +423,10 @@ public void ImportSteamPlusPlusV2(byte[] bytes) try { var bytes_decompress_br = bytes.DecompressByteArrayByBrotli(); - var urls = Serializable.DMP(bytes_decompress_br); - if (urls != null) + var dtos = Serializable.DMP(bytes_decompress_br); + if (dtos.Any_Nullable()) { + var urls = dtos.Select(x => x.ToString()); AuthService.Current.ImportWinAuthenticators(urls, AuthIsLocal, AuthPassword); } } 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 cd8ff2fdee6..b8da75c6fc2 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 @@ -236,10 +236,15 @@ async Task ExportAuthToQRCodeAsync() => await ExportAuthCore(async () => 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 dtos = datas.Select(x => x.ToLightweightExportDTO()).ToArray(); + var bytes = Serializable.SMP(dtos); +#if DEBUG + var bytes_compress_gzip = bytes.CompressByteArray(); +#endif var bytes_compress_br = bytes.CompressByteArrayByBrotli(); +#if DEBUG + Toast.Show($"bytesLength, source: {bytes.Length}, gzip: {bytes_compress_gzip.Length}, br: {bytes_compress_br.Length}"); +#endif return CreateQRCode(bytes_compress_br); }); }, ignorePath: true, ignorePassword: true); diff --git a/src/ST.Services.CloudService/Models/GAPAuthenticators/GAPAuthenticatorDTO.LightweightExportDTO.cs b/src/ST.Services.CloudService/Models/GAPAuthenticators/GAPAuthenticatorDTO.LightweightExportDTO.cs new file mode 100644 index 00000000000..7ff1a78ccce --- /dev/null +++ b/src/ST.Services.CloudService/Models/GAPAuthenticators/GAPAuthenticatorDTO.LightweightExportDTO.cs @@ -0,0 +1,52 @@ +using WinAuth; +using static System.Application.Models.GAPAuthenticatorValueDTO; +using MPKey = MessagePack.KeyAttribute; +using MPObj = MessagePack.MessagePackObjectAttribute; + +namespace System.Application.Models +{ + partial class GAPAuthenticatorDTO + { + /// + /// 轻量化导出模型 + /// + [MPObj] + public sealed class LightweightExportDTO + { + [MPKey(0)] + public GamePlatform Platform { get; set; } + + [MPKey(1)] + public string? Issuer { get; set; } + + [MPKey(2)] + public HMACTypes HMACType { get; set; } + + [MPKey(3)] + public string? Serial { get; set; } + + [MPKey(4)] + public string? DeviceId { get; set; } + + [MPKey(5)] + public string? SteamData { get; set; } + + [MPKey(6)] + public long Counter { get; set; } + + [MPKey(7)] + public int Period { get; set; } + + [MPKey(8)] + public byte[]? SecretKey { get; set; } + + [MPKey(9)] + public int CodeDigits { get; set; } + + [MPKey(10)] + public string Name { get; set; } = string.Empty; + + public override string ToString() => this.ToUrl(); + } + } +} \ No newline at end of file diff --git a/src/ST.Services.CloudService/Models/GAPAuthenticators/GAPAuthenticatorDTO.cs b/src/ST.Services.CloudService/Models/GAPAuthenticators/GAPAuthenticatorDTO.cs index 4dc2166bb49..b65a40509fb 100644 --- a/src/ST.Services.CloudService/Models/GAPAuthenticators/GAPAuthenticatorDTO.cs +++ b/src/ST.Services.CloudService/Models/GAPAuthenticators/GAPAuthenticatorDTO.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。 +#pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。 using MessagePack; using System.Diagnostics.CodeAnalysis; using MPIgnore = MessagePack.IgnoreMemberAttribute; @@ -10,7 +10,7 @@ namespace System.Application.Models { /// [MessagePackObject(keyAsPropertyName: true)] - public class GAPAuthenticatorDTO : IGAPAuthenticatorDTO, IExplicitHasValue + public sealed partial class GAPAuthenticatorDTO : IGAPAuthenticatorDTO, IExplicitHasValue { [MPIgnore, N_JsonIgnore, S_JsonIgnore] public ushort Id { get; set; } diff --git a/src/ST.Services.CloudService/WinAuth/IGAPAuthenticatorDTOExtensions.cs b/src/ST.Services.CloudService/WinAuth/IGAPAuthenticatorDTOExtensions.cs index adf08429c3f..8137609e7fb 100644 --- a/src/ST.Services.CloudService/WinAuth/IGAPAuthenticatorDTOExtensions.cs +++ b/src/ST.Services.CloudService/WinAuth/IGAPAuthenticatorDTOExtensions.cs @@ -1,7 +1,8 @@ using System; +using System.Application; using System.Application.Models; -using System.Text.RegularExpressions; using System.Web; +using static System.Application.Models.GAPAuthenticatorDTO; using static System.Application.Models.GAPAuthenticatorValueDTO; using Base32 = WinAuth.WinAuthBase32; @@ -17,24 +18,25 @@ public static class GAPAuthenticatorDTOExtensions /// /// /// + [Obsolete("use ToLightweightExportDTO")] public static string ToUrl(this IGAPAuthenticatorDTO @this, bool compat = false) { string type = "totp"; string extraparams = string.Empty; - Match match; + //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) + //if (string.IsNullOrEmpty(issuer) && (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) && (match = Regex.Match(label, @"^" + issuer + @"\s+\((.*?)\)(.*)")).Success) + //{ + // label = match.Groups[1].Value + match.Groups[2].Value; + //} + if (!string.IsNullOrEmpty(issuer)) { extraparams += "&issuer=" + HttpUtility.UrlEncode(issuer); } @@ -62,7 +64,7 @@ public static string ToUrl(this IGAPAuthenticatorDTO @this, bool compat = false) extraparams += "&counter=" + hOTPAuthenticator.Counter; } - string secret = HttpUtility.UrlEncode(Base32.GetInstance().Encode(@this.Value.SecretKey ?? Array.Empty())); + var secret = HttpUtility.UrlEncode(Base32.GetInstance().Encode(@this.Value.SecretKey ?? Array.Empty())); if (@this.Value.Period != DEFAULT_PERIOD) { @@ -70,12 +72,134 @@ public static string ToUrl(this IGAPAuthenticatorDTO @this, bool compat = false) } 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)), + !string.IsNullOrEmpty(issuer) ? HttpUtility.UrlPathEncode(issuer) + ":" + HttpUtility.UrlPathEncode(label) : HttpUtility.UrlPathEncode(label), secret, @this.Value.CodeDigits, extraparams); return url; } + + /// + public static LightweightExportDTO ToLightweightExportDTO(this IGAPAuthenticatorDTO @this, bool compat = false) + { + LightweightExportDTO dto = new(); + + //Match match; + var issuer = @this.Value.Issuer; + var label = @this.Name; + //if (string.IsNullOrEmpty(issuer) && (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) && (match = Regex.Match(label, @"^" + issuer + @"\s+\((.*?)\)(.*)")).Success) + //{ + // label = match.Groups[1].Value + match.Groups[2].Value; + //} + if (!string.IsNullOrEmpty(issuer)) + { + dto.Issuer = issuer; + } + + if (@this.Value.HMACType != DEFAULT_HMAC_TYPE) + { + dto.HMACType = @this.Value.HMACType; + } + + if (@this.Value is BattleNetAuthenticator battleNetAuthenticator) + { + dto.Platform = GamePlatform.BattleNet; + dto.Serial = battleNetAuthenticator.Serial; + } + else if (@this.Value is SteamAuthenticator steamAuthenticator) + { + dto.Platform = GamePlatform.Steam; + if (!compat) + { + dto.DeviceId = steamAuthenticator.DeviceId; + dto.SteamData = steamAuthenticator.SteamData; + } + } + else if (@this.Value is HOTPAuthenticator hOTPAuthenticator) + { + dto.Platform = GamePlatform.HOTP; + dto.Counter = hOTPAuthenticator.Counter; + } + + dto.SecretKey = @this.Value.SecretKey; + + if (@this.Value.Period != DEFAULT_PERIOD) + { + dto.Period = @this.Value.Period; + } + + dto.CodeDigits = @this.Value.CodeDigits; + dto.Name = label; + + return dto; + } + + /// + public static string ToUrl(this LightweightExportDTO @this, bool compat = false) + { + string type = "totp"; + string extraparams = string.Empty; + + //Match match; + var issuer = @this.Issuer; + var label = @this.Name; + //if (string.IsNullOrEmpty(issuer) && (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) && (match = Regex.Match(label, @"^" + issuer + @"\s+\((.*?)\)(.*)")).Success) + //{ + // label = match.Groups[1].Value + match.Groups[2].Value; + //} + if (!string.IsNullOrEmpty(issuer)) + { + extraparams += "&issuer=" + HttpUtility.UrlEncode(issuer); + } + + if (@this.HMACType != DEFAULT_HMAC_TYPE) + { + extraparams += "&algorithm=" + @this.HMACType.ToString(); + } + + if (@this.Platform == GamePlatform.BattleNet) + { + extraparams += "&serial=" + HttpUtility.UrlEncode(@this.Serial?.Replace("-", "")); + } + else if (@this.Platform == GamePlatform.Steam) + { + if (!compat) + { + extraparams += "&deviceid=" + HttpUtility.UrlEncode(@this.DeviceId); + extraparams += "&data=" + HttpUtility.UrlEncode(@this.SteamData); + } + } + else if (@this.Platform == GamePlatform.HOTP) + { + type = "hotp"; + extraparams += "&counter=" + @this.Counter; + } + + var secret = HttpUtility.UrlEncode(Base32.GetInstance().Encode(@this.SecretKey ?? Array.Empty())); + + if (@this.Period != DEFAULT_PERIOD) + { + extraparams += "&period=" + @this.Period; + } + + var url = string.Format("otpauth://" + type + "/{0}?secret={1}&digits={2}{3}", + !string.IsNullOrEmpty(issuer) ? HttpUtility.UrlPathEncode(issuer) + ":" + HttpUtility.UrlPathEncode(label) : HttpUtility.UrlPathEncode(label), + secret, + @this.CodeDigits, + extraparams); + + return url; + } } } \ No newline at end of file diff --git a/src/ST/GamePlatform.cs b/src/ST/GamePlatform.cs index 66faab84022..34bb0ccfa85 100644 --- a/src/ST/GamePlatform.cs +++ b/src/ST/GamePlatform.cs @@ -1,4 +1,4 @@ -namespace System.Application +namespace System.Application { public enum GamePlatform : byte { @@ -27,6 +27,14 @@ public enum GamePlatform : byte Google, // Add New ... + + #region WinAuth3 Compat + + HOTP, + + TOTP, + + #endregion } #if DEBUG