Skip to content

Commit

Permalink
Improved Auth QR code format
Browse files Browse the repository at this point in the history
  • Loading branch information
AigioL committed Jul 26, 2021
1 parent 8feb787 commit eb4c142
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 30 deletions.
16 changes: 8 additions & 8 deletions src/ST.Client.Desktop/Services/Mvvm/AuthService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ public void ImportWinAuthenticators(IEnumerable<string> 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
Expand All @@ -181,25 +181,25 @@ public void ImportWinAuthenticators(IEnumerable<string> 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");
}
Expand All @@ -210,7 +210,7 @@ public void ImportWinAuthenticators(IEnumerable<string> 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");
}
Expand Down Expand Up @@ -245,7 +245,7 @@ public void ImportWinAuthenticators(IEnumerable<string> 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;
}
Expand All @@ -259,7 +259,7 @@ public void ImportWinAuthenticators(IEnumerable<string> urls, bool isLocal, stri
{
issuer = string.Empty;
}
else if (string.IsNullOrEmpty(issuer) == false)
else if (!string.IsNullOrEmpty(issuer))
{
auth.Issuer = issuer;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -421,9 +423,10 @@ public void ImportSteamPlusPlusV2(byte[] bytes)
try
{
var bytes_decompress_br = bytes.DecompressByteArrayByBrotli();
var urls = Serializable.DMP<string[]>(bytes_decompress_br);
if (urls != null)
var dtos = Serializable.DMP<LightweightExportDTO[]>(bytes_decompress_br);
if (dtos.Any_Nullable())
{
var urls = dtos.Select(x => x.ToString());
AuthService.Current.ImportWinAuthenticators(urls, AuthIsLocal, AuthPassword);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 轻量化导出模型
/// </summary>
[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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,7 +10,7 @@ namespace System.Application.Models
{
/// <inheritdoc cref="IGAPAuthenticatorDTO"/>
[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; }
Expand Down
152 changes: 138 additions & 14 deletions src/ST.Services.CloudService/WinAuth/IGAPAuthenticatorDTOExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -17,24 +18,25 @@ public static class GAPAuthenticatorDTOExtensions
/// <param name="this"></param>
/// <param name="compat"></param>
/// <returns></returns>
[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);
}
Expand Down Expand Up @@ -62,20 +64,142 @@ 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<byte>()));
var 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)),
!string.IsNullOrEmpty(issuer) ? HttpUtility.UrlPathEncode(issuer) + ":" + HttpUtility.UrlPathEncode(label) : HttpUtility.UrlPathEncode(label),
secret,
@this.Value.CodeDigits,
extraparams);

return url;
}

/// <inheritdoc cref="ToUrl(IGAPAuthenticatorDTO, bool)"/>
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;
}

/// <inheritdoc cref="ToUrl(IGAPAuthenticatorDTO, bool)"/>
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<byte>()));

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;
}
}
}
10 changes: 9 additions & 1 deletion src/ST/GamePlatform.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace System.Application
namespace System.Application
{
public enum GamePlatform : byte
{
Expand Down Expand Up @@ -27,6 +27,14 @@ public enum GamePlatform : byte
Google,

// Add New ...

#region WinAuth3 Compat

HOTP,

TOTP,

#endregion
}

#if DEBUG
Expand Down

0 comments on commit eb4c142

Please sign in to comment.