diff --git a/.gitignore b/.gitignore index 487ea2d..10eee72 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ bin/ obj/ -.idea \ No newline at end of file +.idea +.DS_Store +*.DotSettings.user diff --git a/YandexKeyExtractor/Decryptor.cs b/YandexKeyExtractor/Decryptor.cs index 1b6b45b..28479ce 100644 --- a/YandexKeyExtractor/Decryptor.cs +++ b/YandexKeyExtractor/Decryptor.cs @@ -5,20 +5,28 @@ namespace YandexKeyExtractor; -public static class Decryptor +internal static class Decryptor { + private const int maxStackallocSize = 4096; + public static string? Decrypt(string encryptedText, string password) { var base64Text = NormalizeBase64(encryptedText); - ReadOnlySpan textBytes = Convert.FromBase64String(base64Text).AsSpan(); + var textBytes = Convert.FromBase64String(base64Text); const byte SaltLength = 16; - var textData = textBytes[..^SaltLength]; - var textSalt = textBytes[^SaltLength..]; + var textData = textBytes.AsSpan()[..^SaltLength]; + var salt = textBytes[^SaltLength..]; var generatedPassword = SCrypt.ComputeDerivedKey( - Encoding.UTF8.GetBytes(password), textSalt.ToArray(), 32768, 20, 1, null, 32 + Encoding.UTF8.GetBytes(password), + salt, + cost: 32768, + blockSize: 20, + parallel: 1, + maxThreads: null, + derivedKeyLength: 32 ); using XSalsa20Poly1305 secureBox = new(generatedPassword); @@ -27,8 +35,9 @@ public static class Decryptor var nonce = textData[..NonceLength]; var dataWithMac = textData[NonceLength..]; - - var message = dataWithMac.Length <= 4096 ? stackalloc byte[dataWithMac.Length] : new byte[dataWithMac.Length]; + var message = dataWithMac.Length <= maxStackallocSize + ? stackalloc byte[dataWithMac.Length] + : new byte[dataWithMac.Length]; const byte MacLength = 16; var data = dataWithMac[MacLength..]; @@ -41,11 +50,27 @@ public static class Decryptor private static string NormalizeBase64(string encryptedText) { - return encryptedText.Replace('-', '+').Replace('_', '/') + (encryptedText.Length % 4) switch + var suffixLength = (encryptedText.Length % 4) switch { - 2 => "==", - 3 => "=", - _ => "" + 2 => 2, + 3 => 1, + _ => 0 }; + + var newLength = encryptedText.Length + suffixLength; + var normalized = newLength <= maxStackallocSize / sizeof(char) + ? stackalloc char[newLength] + : new char[newLength]; + + encryptedText.CopyTo(normalized); + normalized.Replace('-', '+'); + normalized.Replace('_', '/'); + + if (suffixLength > 0) + { + normalized[^suffixLength..].Fill('='); + } + + return new string(normalized); } } diff --git a/YandexKeyExtractor/Exceptions/InvalidTrackIdException.cs b/YandexKeyExtractor/Exceptions/InvalidTrackIdException.cs new file mode 100644 index 0000000..4d7269d --- /dev/null +++ b/YandexKeyExtractor/Exceptions/InvalidTrackIdException.cs @@ -0,0 +1,10 @@ +using System; + +namespace YandexKeyExtractor.Exceptions; + +public class InvalidTrackIdException : Exception +{ + public InvalidTrackIdException() : base("Invalid track ID.") + { + } +} diff --git a/YandexKeyExtractor/Exceptions/NoValidBackupException.cs b/YandexKeyExtractor/Exceptions/NoValidBackupException.cs new file mode 100644 index 0000000..5b9455c --- /dev/null +++ b/YandexKeyExtractor/Exceptions/NoValidBackupException.cs @@ -0,0 +1,10 @@ +using System; + +namespace YandexKeyExtractor.Exceptions; + +public class NoValidBackupException : Exception +{ + public NoValidBackupException() : base(Localization.NoValidBackup) + { + } +} diff --git a/YandexKeyExtractor/Exceptions/ResponseFailedException.cs b/YandexKeyExtractor/Exceptions/ResponseFailedException.cs new file mode 100644 index 0000000..d6ab77f --- /dev/null +++ b/YandexKeyExtractor/Exceptions/ResponseFailedException.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace YandexKeyExtractor.Exceptions; + +public class ResponseFailedException : Exception +{ + public string ResponseName { get; } + public string? Status { get; } + public IReadOnlyCollection? Errors { get; } + + public ResponseFailedException(string responseName) : base($"{responseName} failed.") + { + ResponseName = responseName; + } + + public ResponseFailedException(string responseName, string? status, IReadOnlyCollection? errors) : this(responseName) + { + Status = status; + Errors = errors; + } +} diff --git a/YandexKeyExtractor/Models/BackupInfo.cs b/YandexKeyExtractor/Models/BackupInfo.cs new file mode 100644 index 0000000..58177aa --- /dev/null +++ b/YandexKeyExtractor/Models/BackupInfo.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace YandexKeyExtractor.Models; + +public class BackupInfo +{ + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + [JsonPropertyName("updated")] + public uint Updated { get; set; } +} diff --git a/YandexKeyExtractor/Models/BackupInfoResponse.cs b/YandexKeyExtractor/Models/BackupInfoResponse.cs index eddc26a..f91e0ef 100644 --- a/YandexKeyExtractor/Models/BackupInfoResponse.cs +++ b/YandexKeyExtractor/Models/BackupInfoResponse.cs @@ -6,11 +6,4 @@ public class BackupInfoResponse : StatusResponse { [JsonPropertyName("backup_info")] public BackupInfo? Info { get; set; } - - public class BackupInfo - { - [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] - [JsonPropertyName("updated")] - public uint Updated { get; set; } - } } diff --git a/YandexKeyExtractor/Models/CountryResponse.cs b/YandexKeyExtractor/Models/CountryResponse.cs index 940d5ef..0453dd6 100644 --- a/YandexKeyExtractor/Models/CountryResponse.cs +++ b/YandexKeyExtractor/Models/CountryResponse.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Text.Json.Serialization; namespace YandexKeyExtractor.Models; @@ -5,5 +6,5 @@ namespace YandexKeyExtractor.Models; public class CountryResponse : StatusResponse { [JsonPropertyName("country")] - public string[]? Country { get; set; } + public IReadOnlyCollection? Country { get; set; } } diff --git a/YandexKeyExtractor/Models/PhoneNumberInfo.cs b/YandexKeyExtractor/Models/PhoneNumberInfo.cs new file mode 100644 index 0000000..77b751c --- /dev/null +++ b/YandexKeyExtractor/Models/PhoneNumberInfo.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace YandexKeyExtractor.Models; + +public class PhoneNumberInfo +{ + [JsonPropertyName("e164")] + public string? StandardizedNumber { get; set; } +} diff --git a/YandexKeyExtractor/Models/PhoneNumberResponse.cs b/YandexKeyExtractor/Models/PhoneNumberResponse.cs index a7d82e6..23e2726 100644 --- a/YandexKeyExtractor/Models/PhoneNumberResponse.cs +++ b/YandexKeyExtractor/Models/PhoneNumberResponse.cs @@ -6,10 +6,4 @@ public class PhoneNumberResponse : StatusResponse { [JsonPropertyName("number")] public PhoneNumberInfo? PhoneNumber { get; set; } - - public class PhoneNumberInfo - { - [JsonPropertyName("e164")] - public string? StandardizedNumber { get; set; } - } } diff --git a/YandexKeyExtractor/Models/SourceGenerationContext.cs b/YandexKeyExtractor/Models/SourceGenerationContext.cs new file mode 100644 index 0000000..d89b2e0 --- /dev/null +++ b/YandexKeyExtractor/Models/SourceGenerationContext.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace YandexKeyExtractor.Models; + +[JsonSerializable(typeof(BackupInfoResponse))] +[JsonSerializable(typeof(BackupResponse))] +[JsonSerializable(typeof(CountryResponse))] +[JsonSerializable(typeof(PhoneNumberResponse))] +[JsonSerializable(typeof(StatusResponse))] +[JsonSerializable(typeof(TrackResponse))] +[JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] +public partial class SourceGenerationContext : JsonSerializerContext +{ +} diff --git a/YandexKeyExtractor/Models/StatusResponse.cs b/YandexKeyExtractor/Models/StatusResponse.cs index cff1fde..934daa3 100644 --- a/YandexKeyExtractor/Models/StatusResponse.cs +++ b/YandexKeyExtractor/Models/StatusResponse.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Text.Json.Serialization; namespace YandexKeyExtractor.Models; @@ -8,7 +9,7 @@ public class StatusResponse public string? Status { get; set; } [JsonPropertyName("errors")] - public string[]? Errors { get; set; } + public IReadOnlyCollection? Errors { get; set; } public bool IsSuccess => Status == "ok"; } diff --git a/YandexKeyExtractor/Program.cs b/YandexKeyExtractor/Program.cs index 536f635..32fcace 100644 --- a/YandexKeyExtractor/Program.cs +++ b/YandexKeyExtractor/Program.cs @@ -1,65 +1,85 @@ using System; +using System.Globalization; using System.IO; +using System.Threading.Tasks; using YandexKeyExtractor; +using YandexKeyExtractor.Exceptions; -Console.WriteLine("Initializing..."); -using var handler = WebHandler.Create(); +CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("ru-RU"); +Console.WriteLine(Localization.Initializing); +using var handler = new WebHandler(); -var country = await handler.TryGetCountry(); - -PromptInput(out var phoneNumber, "phone number"); - -phoneNumber = phoneNumber.TrimStart('+'); -var phone = await handler.GetPhoneNumberInfo(phoneNumber, country); - -var trackID = await handler.SendSMSCodeAndGetTrackID(phone, country); -if (string.IsNullOrEmpty(trackID)) +string backup; +try { - return; -} - -PromptInput(out var smsCode, "SMS code"); - -if (!await handler.CheckCode(smsCode, trackID)) + backup = await RetrieveBackup(handler); +} catch (ResponseFailedException e) { - return; -} + if (e.Status == null) + { + Console.WriteLine(Localization.ResponseFailed, e.ResponseName); + } else + { + Console.WriteLine(Localization.ResponseFailedWithDetails, e.Status, e.ResponseName, string.Join(", ", e.Errors ?? [])); + } -if (!await handler.ValidateBackupInfo(phone, trackID, country)) -{ return; -} - -var backup = await handler.GetBackupData(phone, trackID); -if (string.IsNullOrEmpty(backup)) +} catch (NoValidBackupException) { + Console.WriteLine(Localization.NoValidBackup); + return; +} catch (Exception e) +{ + Console.WriteLine(Localization.UnknownErrorOccurred, e.Message); + + throw; } -PromptInput(out var backupPassword, "backup password"); +PromptInput(out var backupPassword, Localization.BackupPasswordVariableName); -Console.WriteLine("Decrypting..."); +Console.WriteLine(Localization.Decrypting); var message = Decryptor.Decrypt(backup, backupPassword); if (string.IsNullOrEmpty(message)) { - Console.WriteLine("Decryption failed! Most likely the password is wrong"); + Console.WriteLine(Localization.DecryptionFailed); return; } -Console.WriteLine("Successfully decrypted!"); await File.WriteAllTextAsync("result.txt", message); -Console.WriteLine($"Written {message.Split('\n').Length} authenticators to the file (result.txt)"); +Console.WriteLine(Localization.Success, message.AsSpan().Count('\n') + 1); return; -static void PromptInput(out string result, string argumentName = "") +static async Task RetrieveBackup(WebHandler handler) +{ + var country = await handler.TryGetCountry() ?? "ru"; + + PromptInput(out var phoneNumber, Localization.PhoneNumberVariableName); + + phoneNumber = phoneNumber.TrimStart('+'); + var phone = await handler.GetPhoneNumberInfo(phoneNumber, country); + + var trackID = await handler.SendSMSCodeAndGetTrackID(phone, country); + + PromptInput(out var smsCode, Localization.SmsCodeVariableName); + + await handler.CheckCode(smsCode, trackID); + await handler.ValidateBackupInfo(phone, trackID, country); + + var backup = await handler.GetBackupData(phone, trackID); + + return backup; +} + +static void PromptInput(out string result, string argumentName) { - Console.WriteLine($"Enter {argumentName}:"); + Console.WriteLine(Localization.PromptVariable, argumentName.ToLower(CultureInfo.CurrentCulture)); var input = Console.ReadLine(); while (string.IsNullOrEmpty(input)) { - Console.WriteLine($"{argumentName} is invalid, try again:"); + Console.WriteLine(Localization.InvalidVariableValue, argumentName); input = Console.ReadLine(); } diff --git a/YandexKeyExtractor/Resources/Localization.Designer.cs b/YandexKeyExtractor/Resources/Localization.Designer.cs new file mode 100644 index 0000000..19ee92f --- /dev/null +++ b/YandexKeyExtractor/Resources/Localization.Designer.cs @@ -0,0 +1,126 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace YandexKeyExtractor { + using System; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Localization { + + private static System.Resources.ResourceManager resourceMan; + + private static System.Globalization.CultureInfo resourceCulture; + + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Localization() { + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { + get { + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("YandexKeyExtractor.Resources.Localization", typeof(Localization).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + internal static string Initializing { + get { + return ResourceManager.GetString("Initializing", resourceCulture); + } + } + + internal static string PhoneNumberVariableName { + get { + return ResourceManager.GetString("PhoneNumberVariableName", resourceCulture); + } + } + + internal static string PromptVariable { + get { + return ResourceManager.GetString("PromptVariable", resourceCulture); + } + } + + internal static string InvalidVariableValue { + get { + return ResourceManager.GetString("InvalidVariableValue", resourceCulture); + } + } + + internal static string SmsCodeVariableName { + get { + return ResourceManager.GetString("SmsCodeVariableName", resourceCulture); + } + } + + internal static string BackupPasswordVariableName { + get { + return ResourceManager.GetString("BackupPasswordVariableName", resourceCulture); + } + } + + internal static string Decrypting { + get { + return ResourceManager.GetString("Decrypting", resourceCulture); + } + } + + internal static string DecryptionFailed { + get { + return ResourceManager.GetString("DecryptionFailed", resourceCulture); + } + } + + internal static string Success { + get { + return ResourceManager.GetString("Success", resourceCulture); + } + } + + internal static string NoValidBackup { + get { + return ResourceManager.GetString("NoValidBackup", resourceCulture); + } + } + + internal static string ResponseFailed { + get { + return ResourceManager.GetString("ResponseFailed", resourceCulture); + } + } + + internal static string ResponseFailedWithDetails { + get { + return ResourceManager.GetString("ResponseFailedWithDetails", resourceCulture); + } + } + + internal static string UnknownErrorOccurred { + get { + return ResourceManager.GetString("UnknownErrorOccurred", resourceCulture); + } + } + } +} diff --git a/YandexKeyExtractor/Resources/Localization.resx b/YandexKeyExtractor/Resources/Localization.resx new file mode 100644 index 0000000..accef49 --- /dev/null +++ b/YandexKeyExtractor/Resources/Localization.resx @@ -0,0 +1,60 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Initializing... + + + Phone number + + + Enter {0}: + + + {0} is invalid, try again: + + + SMS code + + + Backup password + + + Decrypting... + + + Decryption failed! Most likely the password is wrong. + + + Success! Written {0} authenticators to the file (result.txt). + + + Couldn't find a valid backup! + + + Got an error while requesting {0}. + + + Got an error status '{0}' while requesting {1}, errors: {2}. + + + An unknown error occurred: {0}. + + diff --git a/YandexKeyExtractor/Resources/Localization.ru-RU.Designer.cs b/YandexKeyExtractor/Resources/Localization.ru-RU.Designer.cs new file mode 100644 index 0000000..a4606d3 --- /dev/null +++ b/YandexKeyExtractor/Resources/Localization.ru-RU.Designer.cs @@ -0,0 +1,126 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace YandexKeyExtractor { + using System; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Localization_ru_RU { + + private static System.Resources.ResourceManager resourceMan; + + private static System.Globalization.CultureInfo resourceCulture; + + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Localization_ru_RU() { + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { + get { + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("YandexKeyExtractor.Localization_ru_RU", typeof(Localization_ru_RU).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + internal static string Initializing { + get { + return ResourceManager.GetString("Initializing", resourceCulture); + } + } + + internal static string PhoneNumberVariableName { + get { + return ResourceManager.GetString("PhoneNumberVariableName", resourceCulture); + } + } + + internal static string PromptVariable { + get { + return ResourceManager.GetString("PromptVariable", resourceCulture); + } + } + + internal static string InvalidVariableValue { + get { + return ResourceManager.GetString("InvalidVariableValue", resourceCulture); + } + } + + internal static string SmsCodeVariableName { + get { + return ResourceManager.GetString("SmsCodeVariableName", resourceCulture); + } + } + + internal static string BackupPasswordVariableName { + get { + return ResourceManager.GetString("BackupPasswordVariableName", resourceCulture); + } + } + + internal static string Decrypting { + get { + return ResourceManager.GetString("Decrypting", resourceCulture); + } + } + + internal static string DecryptionFailed { + get { + return ResourceManager.GetString("DecryptionFailed", resourceCulture); + } + } + + internal static string Success { + get { + return ResourceManager.GetString("Success", resourceCulture); + } + } + + internal static string NoValidBackup { + get { + return ResourceManager.GetString("NoValidBackup", resourceCulture); + } + } + + internal static string ResponseFailed { + get { + return ResourceManager.GetString("ResponseFailed", resourceCulture); + } + } + + internal static string ResponseFailedWithDetails { + get { + return ResourceManager.GetString("ResponseFailedWithDetails", resourceCulture); + } + } + + internal static string UnknownErrorOccurred { + get { + return ResourceManager.GetString("UnknownErrorOccurred", resourceCulture); + } + } + } +} diff --git a/YandexKeyExtractor/Resources/Localization.ru-RU.resx b/YandexKeyExtractor/Resources/Localization.ru-RU.resx new file mode 100644 index 0000000..f1e37cc --- /dev/null +++ b/YandexKeyExtractor/Resources/Localization.ru-RU.resx @@ -0,0 +1,60 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Загрузка... + + + Номер телефона + + + Введите {0}: + + + {0} неверен, попробуйте ещё раз: + + + Код из SMS + + + Пароль резервной копии + + + Дешифрование... + + + Дешифрование не удалось! Скорее всего, был введён неправильный пароль. + + + Успех! Записано {0} данных аутентификаторов в файл (result.txt). + + + Не получилось найти действительную резервную копию! + + + Получена ошибка при запросе {0}. + + + Получен код ошибки '{0} при запросе {1}, ошибки: {2}. + + + Произошла неизвестная ошибка: {0}. + + diff --git a/YandexKeyExtractor/WebHandler.cs b/YandexKeyExtractor/WebHandler.cs index 75d12ab..e9c2cea 100644 --- a/YandexKeyExtractor/WebHandler.cs +++ b/YandexKeyExtractor/WebHandler.cs @@ -1,197 +1,130 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; +using System.Net.Http.Json; using System.Runtime.CompilerServices; using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading.Tasks; -using Flurl.Http; -using Flurl.Http.Configuration; +using YandexKeyExtractor.Exceptions; using YandexKeyExtractor.Models; namespace YandexKeyExtractor; public sealed class WebHandler : IDisposable { - private WebHandler(IFlurlClient client) => Client = client; - private IFlurlClient Client { get; } - - public void Dispose() + private readonly HttpClient _client = new() { - Client.Dispose(); - } + DefaultRequestHeaders = {UserAgent = {new ProductInfoHeaderValue("okhttp", "2.7.5")}}, + BaseAddress = new Uri("https://registrator.mobile.yandex.net/1/") + }; - public async Task CheckCode(string? smsCode, string? trackID) + private readonly JsonSerializerOptions _jsonSettings = new() { - var checkCodeResponse = await Client.Request("/bundle/yakey_backup/check_code/") - .PostUrlEncodedAsync( - new - { - code = smsCode, - track_id = trackID - } - ) - .ReceiveJson(); - - return ValidateResponse(checkCodeResponse, nameof(checkCodeResponse)); - } + TypeInfoResolver = SourceGenerationContext.Default + }; - public static WebHandler Create() + public async Task CheckCode(string smsCode, string trackID) { - JsonSerializerOptions jsonSettings = new() - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - - var client = new FlurlClient( - new HttpClient - { - DefaultRequestHeaders = {UserAgent = {new ProductInfoHeaderValue("okhttp", "2.7.5")}}, - BaseAddress = new Uri("https://registrator.mobile.yandex.net/1/") - }); + var checkCodeResponse = await PostUrlEncodedAndReceiveJson( + new Uri("bundle/yakey_backup/check_code/", UriKind.Relative), + new Dictionary(2) {["code"] = smsCode, ["track_id"] = trackID}); - client.Settings.JsonSerializer = new DefaultJsonSerializer(jsonSettings); - - return new WebHandler(client); + ValidateResponse(checkCodeResponse); } - public async Task GetBackupData(string phone, string? trackID) + public async Task GetBackupData(string phone, string trackID) { - var backupResponse = await Client.Request("/bundle/yakey_backup/download") - .PostUrlEncodedAsync( - new - { - number = phone, - track_id = trackID - } - ) - .ReceiveJson(); - - if (!ValidateResponse(backupResponse, nameof(backupResponse))) - { - return null; - } + var backupResponse = await PostUrlEncodedAndReceiveJson( + new Uri("bundle/yakey_backup/download", UriKind.Relative), + new Dictionary(2) {["number"] = phone, ["track_id"] = trackID}); + + ValidateResponse(backupResponse); if (string.IsNullOrEmpty(backupResponse.Backup)) { - Console.WriteLine("Fatal error - Couldn't find valid backup!"); - - return null; + throw new NoValidBackupException(); } return backupResponse.Backup; } - public async Task GetPhoneNumberInfo(string? phoneNumber, string country) + public async Task GetPhoneNumberInfo(string phoneNumber, string country) { - var phoneNumberResponse = await Client.Request("/bundle/validate/phone_number/") - .PostUrlEncodedAsync( - new - { - phone_number = phoneNumber, - country - }) - .ReceiveJson(); - - ValidateResponse(phoneNumberResponse, nameof(phoneNumberResponse)); + var phoneNumberResponse = await PostUrlEncodedAndReceiveJson( + new Uri("bundle/validate/phone_number/", UriKind.Relative), + new Dictionary(2) {["phone_number"] = phoneNumber, ["country"] = country}); - var phone = phoneNumberResponse?.PhoneNumber?.StandardizedNumber ?? '+' + phoneNumber; + var phone = phoneNumberResponse?.PhoneNumber?.StandardizedNumber ?? $"+{phoneNumber}"; return phone; } - public async Task SendSMSCodeAndGetTrackID(string phone, string country) + public async Task SendSMSCodeAndGetTrackID(string phone, string country) { - var trackResponse = await Client.Request("/bundle/yakey_backup/send_code/") - .PostUrlEncodedAsync( - new - { - display_language = "en", - number = phone, - country - }) - .ReceiveJson(); - - if (!ValidateResponse(trackResponse, nameof(trackResponse))) - { - return null; - } + var trackResponse = await PostUrlEncodedAndReceiveJson( + new Uri("bundle/yakey_backup/send_code/", UriKind.Relative), + new Dictionary(3) {["display_language"] = "en", ["number"] = phone, ["country"] = country}); + + ValidateResponse(trackResponse); var trackID = trackResponse.TrackID; if (string.IsNullOrEmpty(trackID)) { - Console.WriteLine("Track ID is empty!"); - - return null; + throw new InvalidTrackIdException(); } return trackID; } - public async Task TryGetCountry() + public async Task TryGetCountry() { - var countryResponse = await Client.Request("/suggest/country") - .GetAsync() - .ReceiveJson(); + var countryResponse = await _client.GetFromJsonAsync(new Uri("suggest/country", UriKind.Relative)); - ValidateResponse(countryResponse, nameof(countryResponse)); - - var country = countryResponse?.Country?.FirstOrDefault() ?? "ru"; - - return country; + return countryResponse?.Country?.FirstOrDefault(); } - public async Task ValidateBackupInfo(string phone, string? trackID, string country) + public async Task ValidateBackupInfo(string phone, string trackID, string country) { - var backupInfoResponse = await Client.Request("/bundle/yakey_backup/info/") - .PostUrlEncodedAsync( - new - { - number = phone, - track_id = trackID, - country - }) - .ReceiveJson(); - - if (!ValidateResponse(backupInfoResponse, nameof(backupInfoResponse))) - { - return false; - } + var backupInfoResponse = await PostUrlEncodedAndReceiveJson( + new Uri("bundle/yakey_backup/info/", UriKind.Relative), + new Dictionary(3) {["number"] = phone, ["track_id"] = trackID, ["country"] = country}); + + ValidateResponse(backupInfoResponse); if (backupInfoResponse.Info?.Updated == null) { - Console.WriteLine("Fatal error - Couldn't find valid backup!"); - - return false; + throw new NoValidBackupException(); } - - return true; } + private async Task PostUrlEncodedAndReceiveJson(Uri url, Dictionary data) + { + using var content = new FormUrlEncodedContent(data); + using var responseMessage = await _client.PostAsync(url, content); + responseMessage.EnsureSuccessStatusCode(); - private static bool ValidateResponse([NotNullWhen(true)] T? response, - [CallerArgumentExpression("response")] string responseName = "") where T : StatusResponse + return (await responseMessage.Content.ReadFromJsonAsync(_jsonSettings))!; + } + + private static void ValidateResponse([NotNull] T? response, + [CallerArgumentExpression(nameof(response))] string responseName = "") where T : StatusResponse { if (response == null) { - Console.WriteLine(responseName + " failed!"); - - return false; + throw new ResponseFailedException(responseName); } if (!response.IsSuccess) { - Console.WriteLine(responseName + $" failed with error {response.Status}!"); - if (response.Errors != null) - { - Console.WriteLine("Errors: " + string.Join(',', response.Errors)); - } - - return false; + throw new ResponseFailedException(responseName, response.Status, response.Errors); } + } - return true; + public void Dispose() + { + _client.Dispose(); } } diff --git a/YandexKeyExtractor/YandexKeyExtractor.csproj b/YandexKeyExtractor/YandexKeyExtractor.csproj index 880e5f5..5a2be28 100644 --- a/YandexKeyExtractor/YandexKeyExtractor.csproj +++ b/YandexKeyExtractor/YandexKeyExtractor.csproj @@ -1,27 +1,52 @@ - - 1.0.0.0 - 1.0.0.0 - enable - Exe - net8.0 - + + AllEnabledByDefault + 1.1.0 + 1.1.0 + $(NoWarn);CA1032;CA2007 + enable + Exe + net8.0 + - - - false - false - false - false - false - link - + + + false + false + false + false + false + link + - - - - - + + + + + + + + <_Parameter1>en-US + + + ResXFileCodeGenerator + Localization.Designer.cs + + + True + True + Localization.resx + + + ResXFileCodeGenerator + Localization.ru-RU.Designer.cs + + + True + True + Localization.ru-RU.resx + +