Add localization, optimization, better error handling

This commit is contained in:
Vita Chumakova
2024-10-03 05:21:18 +04:00
parent a4778c2883
commit e02654a4d1
19 changed files with 655 additions and 214 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
bin/ bin/
obj/ obj/
.idea .idea
.DS_Store
*.DotSettings.user

View File

@@ -5,20 +5,28 @@ using NaCl;
namespace YandexKeyExtractor; namespace YandexKeyExtractor;
public static class Decryptor internal static class Decryptor
{ {
private const int maxStackallocSize = 4096;
public static string? Decrypt(string encryptedText, string password) public static string? Decrypt(string encryptedText, string password)
{ {
var base64Text = NormalizeBase64(encryptedText); var base64Text = NormalizeBase64(encryptedText);
ReadOnlySpan<byte> textBytes = Convert.FromBase64String(base64Text).AsSpan(); var textBytes = Convert.FromBase64String(base64Text);
const byte SaltLength = 16; const byte SaltLength = 16;
var textData = textBytes[..^SaltLength]; var textData = textBytes.AsSpan()[..^SaltLength];
var textSalt = textBytes[^SaltLength..]; var salt = textBytes[^SaltLength..];
var generatedPassword = SCrypt.ComputeDerivedKey( 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); using XSalsa20Poly1305 secureBox = new(generatedPassword);
@@ -27,8 +35,9 @@ public static class Decryptor
var nonce = textData[..NonceLength]; var nonce = textData[..NonceLength];
var dataWithMac = textData[NonceLength..]; var dataWithMac = textData[NonceLength..];
var message = dataWithMac.Length <= maxStackallocSize
var message = dataWithMac.Length <= 4096 ? stackalloc byte[dataWithMac.Length] : new byte[dataWithMac.Length]; ? stackalloc byte[dataWithMac.Length]
: new byte[dataWithMac.Length];
const byte MacLength = 16; const byte MacLength = 16;
var data = dataWithMac[MacLength..]; var data = dataWithMac[MacLength..];
@@ -41,11 +50,27 @@ public static class Decryptor
private static string NormalizeBase64(string encryptedText) private static string NormalizeBase64(string encryptedText)
{ {
return encryptedText.Replace('-', '+').Replace('_', '/') + (encryptedText.Length % 4) switch var suffixLength = (encryptedText.Length % 4) switch
{ {
2 => "==", 2 => 2,
3 => "=", 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);
} }
} }

View File

@@ -0,0 +1,10 @@
using System;
namespace YandexKeyExtractor.Exceptions;
public class InvalidTrackIdException : Exception
{
public InvalidTrackIdException() : base("Invalid track ID.")
{
}
}

View File

@@ -0,0 +1,10 @@
using System;
namespace YandexKeyExtractor.Exceptions;
public class NoValidBackupException : Exception
{
public NoValidBackupException() : base(Localization.NoValidBackup)
{
}
}

View File

@@ -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<string>? Errors { get; }
public ResponseFailedException(string responseName) : base($"{responseName} failed.")
{
ResponseName = responseName;
}
public ResponseFailedException(string responseName, string? status, IReadOnlyCollection<string>? errors) : this(responseName)
{
Status = status;
Errors = errors;
}
}

View File

@@ -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; }
}

View File

@@ -6,11 +6,4 @@ public class BackupInfoResponse : StatusResponse
{ {
[JsonPropertyName("backup_info")] [JsonPropertyName("backup_info")]
public BackupInfo? Info { get; set; } public BackupInfo? Info { get; set; }
public class BackupInfo
{
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
[JsonPropertyName("updated")]
public uint Updated { get; set; }
}
} }

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace YandexKeyExtractor.Models; namespace YandexKeyExtractor.Models;
@@ -5,5 +6,5 @@ namespace YandexKeyExtractor.Models;
public class CountryResponse : StatusResponse public class CountryResponse : StatusResponse
{ {
[JsonPropertyName("country")] [JsonPropertyName("country")]
public string[]? Country { get; set; } public IReadOnlyCollection<string>? Country { get; set; }
} }

View File

@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace YandexKeyExtractor.Models;
public class PhoneNumberInfo
{
[JsonPropertyName("e164")]
public string? StandardizedNumber { get; set; }
}

View File

@@ -6,10 +6,4 @@ public class PhoneNumberResponse : StatusResponse
{ {
[JsonPropertyName("number")] [JsonPropertyName("number")]
public PhoneNumberInfo? PhoneNumber { get; set; } public PhoneNumberInfo? PhoneNumber { get; set; }
public class PhoneNumberInfo
{
[JsonPropertyName("e164")]
public string? StandardizedNumber { get; set; }
}
} }

View File

@@ -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
{
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace YandexKeyExtractor.Models; namespace YandexKeyExtractor.Models;
@@ -8,7 +9,7 @@ public class StatusResponse
public string? Status { get; set; } public string? Status { get; set; }
[JsonPropertyName("errors")] [JsonPropertyName("errors")]
public string[]? Errors { get; set; } public IReadOnlyCollection<string>? Errors { get; set; }
public bool IsSuccess => Status == "ok"; public bool IsSuccess => Status == "ok";
} }

View File

@@ -1,65 +1,85 @@
using System; using System;
using System.Globalization;
using System.IO; using System.IO;
using System.Threading.Tasks;
using YandexKeyExtractor; using YandexKeyExtractor;
using YandexKeyExtractor.Exceptions;
Console.WriteLine("Initializing..."); CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("ru-RU");
using var handler = WebHandler.Create(); Console.WriteLine(Localization.Initializing);
using var handler = new WebHandler();
var country = await handler.TryGetCountry(); string backup;
try
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))
{ {
backup = await RetrieveBackup(handler);
} catch (ResponseFailedException e)
{
if (e.Status == null)
{
Console.WriteLine(Localization.ResponseFailed, e.ResponseName);
} else
{
Console.WriteLine(Localization.ResponseFailedWithDetails, e.Status, e.ResponseName, string.Join(", ", e.Errors ?? []));
}
return; return;
} catch (NoValidBackupException)
{
Console.WriteLine(Localization.NoValidBackup);
return;
} catch (Exception e)
{
Console.WriteLine(Localization.UnknownErrorOccurred, e.Message);
throw;
} }
PromptInput(out var smsCode, "SMS code"); PromptInput(out var backupPassword, Localization.BackupPasswordVariableName);
if (!await handler.CheckCode(smsCode, trackID)) Console.WriteLine(Localization.Decrypting);
{
return;
}
if (!await handler.ValidateBackupInfo(phone, trackID, country))
{
return;
}
var backup = await handler.GetBackupData(phone, trackID);
if (string.IsNullOrEmpty(backup))
{
return;
}
PromptInput(out var backupPassword, "backup password");
Console.WriteLine("Decrypting...");
var message = Decryptor.Decrypt(backup, backupPassword); var message = Decryptor.Decrypt(backup, backupPassword);
if (string.IsNullOrEmpty(message)) if (string.IsNullOrEmpty(message))
{ {
Console.WriteLine("Decryption failed! Most likely the password is wrong"); Console.WriteLine(Localization.DecryptionFailed);
return; return;
} }
Console.WriteLine("Successfully decrypted!");
await File.WriteAllTextAsync("result.txt", message); 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; return;
static void PromptInput(out string result, string argumentName = "") static async Task<string> RetrieveBackup(WebHandler handler)
{ {
Console.WriteLine($"Enter {argumentName}:"); 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(Localization.PromptVariable, argumentName.ToLower(CultureInfo.CurrentCulture));
var input = Console.ReadLine(); var input = Console.ReadLine();
while (string.IsNullOrEmpty(input)) while (string.IsNullOrEmpty(input))
{ {
Console.WriteLine($"{argumentName} is invalid, try again:"); Console.WriteLine(Localization.InvalidVariableValue, argumentName);
input = Console.ReadLine(); input = Console.ReadLine();
} }

View File

@@ -0,0 +1,126 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
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);
}
}
}
}

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Initializing" xml:space="preserve">
<value>Initializing...</value>
</data>
<data name="PhoneNumberVariableName" xml:space="preserve">
<value>Phone number</value>
</data>
<data name="PromptVariable" xml:space="preserve">
<value>Enter {0}:</value>
</data>
<data name="InvalidVariableValue" xml:space="preserve">
<value>{0} is invalid, try again:</value>
</data>
<data name="SmsCodeVariableName" xml:space="preserve">
<value>SMS code</value>
</data>
<data name="BackupPasswordVariableName" xml:space="preserve">
<value>Backup password</value>
</data>
<data name="Decrypting" xml:space="preserve">
<value>Decrypting...</value>
</data>
<data name="DecryptionFailed" xml:space="preserve">
<value>Decryption failed! Most likely the password is wrong.</value>
</data>
<data name="Success" xml:space="preserve">
<value>Success! Written {0} authenticators to the file (result.txt).</value>
</data>
<data name="NoValidBackup" xml:space="preserve">
<value>Couldn't find a valid backup!</value>
</data>
<data name="ResponseFailed" xml:space="preserve">
<value>Got an error while requesting {0}.</value>
</data>
<data name="ResponseFailedWithDetails" xml:space="preserve">
<value>Got an error status '{0}' while requesting {1}, errors: {2}.</value>
</data>
<data name="UnknownErrorOccurred" xml:space="preserve">
<value>An unknown error occurred: {0}.</value>
</data>
</root>

View File

@@ -0,0 +1,126 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
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);
}
}
}
}

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Initializing" xml:space="preserve">
<value>Загрузка...</value>
</data>
<data name="PhoneNumberVariableName" xml:space="preserve">
<value>Номер телефона</value>
</data>
<data name="PromptVariable" xml:space="preserve">
<value>Введите {0}:</value>
</data>
<data name="InvalidVariableValue" xml:space="preserve">
<value>{0} неверен, попробуйте ещё раз:</value>
</data>
<data name="SmsCodeVariableName" xml:space="preserve">
<value>Код из SMS</value>
</data>
<data name="BackupPasswordVariableName" xml:space="preserve">
<value>Пароль резервной копии</value>
</data>
<data name="Decrypting" xml:space="preserve">
<value>Дешифрование...</value>
</data>
<data name="DecryptionFailed" xml:space="preserve">
<value>Дешифрование не удалось! Скорее всего, был введён неправильный пароль.</value>
</data>
<data name="Success" xml:space="preserve">
<value>Успех! Записано {0} данных аутентификаторов в файл (result.txt).</value>
</data>
<data name="NoValidBackup" xml:space="preserve">
<value>Не получилось найти действительную резервную копию!</value>
</data>
<data name="ResponseFailed" xml:space="preserve">
<value>Получена ошибка при запросе {0}.</value>
</data>
<data name="ResponseFailedWithDetails" xml:space="preserve">
<value>Получен код ошибки '{0} при запросе {1}, ошибки: {2}.</value>
</data>
<data name="UnknownErrorOccurred" xml:space="preserve">
<value>Произошла неизвестная ошибка: {0}.</value>
</data>
</root>

View File

@@ -1,197 +1,130 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Flurl.Http; using YandexKeyExtractor.Exceptions;
using Flurl.Http.Configuration;
using YandexKeyExtractor.Models; using YandexKeyExtractor.Models;
namespace YandexKeyExtractor; namespace YandexKeyExtractor;
public sealed class WebHandler : IDisposable public sealed class WebHandler : IDisposable
{ {
private WebHandler(IFlurlClient client) => Client = client; private readonly HttpClient _client = new()
private IFlurlClient Client { get; }
public void Dispose()
{
Client.Dispose();
}
public async Task<bool> CheckCode(string? smsCode, string? trackID)
{
var checkCodeResponse = await Client.Request("/bundle/yakey_backup/check_code/")
.PostUrlEncodedAsync(
new
{
code = smsCode,
track_id = trackID
}
)
.ReceiveJson<StatusResponse?>();
return ValidateResponse(checkCodeResponse, nameof(checkCodeResponse));
}
public static WebHandler Create()
{
JsonSerializerOptions jsonSettings = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
var client = new FlurlClient(
new HttpClient
{ {
DefaultRequestHeaders = {UserAgent = {new ProductInfoHeaderValue("okhttp", "2.7.5")}}, DefaultRequestHeaders = {UserAgent = {new ProductInfoHeaderValue("okhttp", "2.7.5")}},
BaseAddress = new Uri("https://registrator.mobile.yandex.net/1/") BaseAddress = new Uri("https://registrator.mobile.yandex.net/1/")
}); };
client.Settings.JsonSerializer = new DefaultJsonSerializer(jsonSettings); private readonly JsonSerializerOptions _jsonSettings = new()
{
TypeInfoResolver = SourceGenerationContext.Default
};
return new WebHandler(client); public async Task CheckCode(string smsCode, string trackID)
{
var checkCodeResponse = await PostUrlEncodedAndReceiveJson<StatusResponse>(
new Uri("bundle/yakey_backup/check_code/", UriKind.Relative),
new Dictionary<string, string>(2) {["code"] = smsCode, ["track_id"] = trackID});
ValidateResponse(checkCodeResponse);
} }
public async Task<string?> GetBackupData(string phone, string? trackID) public async Task<string> GetBackupData(string phone, string trackID)
{ {
var backupResponse = await Client.Request("/bundle/yakey_backup/download") var backupResponse = await PostUrlEncodedAndReceiveJson<BackupResponse>(
.PostUrlEncodedAsync( new Uri("bundle/yakey_backup/download", UriKind.Relative),
new new Dictionary<string, string>(2) {["number"] = phone, ["track_id"] = trackID});
{
number = phone,
track_id = trackID
}
)
.ReceiveJson<BackupResponse?>();
if (!ValidateResponse(backupResponse, nameof(backupResponse))) ValidateResponse(backupResponse);
{
return null;
}
if (string.IsNullOrEmpty(backupResponse.Backup)) if (string.IsNullOrEmpty(backupResponse.Backup))
{ {
Console.WriteLine("Fatal error - Couldn't find valid backup!"); throw new NoValidBackupException();
return null;
} }
return backupResponse.Backup; return backupResponse.Backup;
} }
public async Task<string> GetPhoneNumberInfo(string? phoneNumber, string country) public async Task<string> GetPhoneNumberInfo(string phoneNumber, string country)
{ {
var phoneNumberResponse = await Client.Request("/bundle/validate/phone_number/") var phoneNumberResponse = await PostUrlEncodedAndReceiveJson<PhoneNumberResponse>(
.PostUrlEncodedAsync( new Uri("bundle/validate/phone_number/", UriKind.Relative),
new new Dictionary<string, string>(2) {["phone_number"] = phoneNumber, ["country"] = country});
{
phone_number = phoneNumber,
country
})
.ReceiveJson<PhoneNumberResponse?>();
ValidateResponse(phoneNumberResponse, nameof(phoneNumberResponse)); var phone = phoneNumberResponse?.PhoneNumber?.StandardizedNumber ?? $"+{phoneNumber}";
var phone = phoneNumberResponse?.PhoneNumber?.StandardizedNumber ?? '+' + phoneNumber;
return phone; return phone;
} }
public async Task<string?> SendSMSCodeAndGetTrackID(string phone, string country) public async Task<string> SendSMSCodeAndGetTrackID(string phone, string country)
{ {
var trackResponse = await Client.Request("/bundle/yakey_backup/send_code/") var trackResponse = await PostUrlEncodedAndReceiveJson<TrackResponse>(
.PostUrlEncodedAsync( new Uri("bundle/yakey_backup/send_code/", UriKind.Relative),
new new Dictionary<string, string>(3) {["display_language"] = "en", ["number"] = phone, ["country"] = country});
{
display_language = "en",
number = phone,
country
})
.ReceiveJson<TrackResponse?>();
if (!ValidateResponse(trackResponse, nameof(trackResponse))) ValidateResponse(trackResponse);
{
return null;
}
var trackID = trackResponse.TrackID; var trackID = trackResponse.TrackID;
if (string.IsNullOrEmpty(trackID)) if (string.IsNullOrEmpty(trackID))
{ {
Console.WriteLine("Track ID is empty!"); throw new InvalidTrackIdException();
return null;
} }
return trackID; return trackID;
} }
public async Task<string> TryGetCountry() public async Task<string?> TryGetCountry()
{ {
var countryResponse = await Client.Request("/suggest/country") var countryResponse = await _client.GetFromJsonAsync<CountryResponse>(new Uri("suggest/country", UriKind.Relative));
.GetAsync()
.ReceiveJson<CountryResponse?>();
ValidateResponse(countryResponse, nameof(countryResponse)); return countryResponse?.Country?.FirstOrDefault();
var country = countryResponse?.Country?.FirstOrDefault() ?? "ru";
return country;
} }
public async Task<bool> 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/") var backupInfoResponse = await PostUrlEncodedAndReceiveJson<BackupInfoResponse>(
.PostUrlEncodedAsync( new Uri("bundle/yakey_backup/info/", UriKind.Relative),
new new Dictionary<string, string>(3) {["number"] = phone, ["track_id"] = trackID, ["country"] = country});
{
number = phone,
track_id = trackID,
country
})
.ReceiveJson<BackupInfoResponse?>();
if (!ValidateResponse(backupInfoResponse, nameof(backupInfoResponse))) ValidateResponse(backupInfoResponse);
{
return false;
}
if (backupInfoResponse.Info?.Updated == null) if (backupInfoResponse.Info?.Updated == null)
{ {
Console.WriteLine("Fatal error - Couldn't find valid backup!"); throw new NoValidBackupException();
}
return false;
} }
return true; private async Task<T?> PostUrlEncodedAndReceiveJson<T>(Uri url, Dictionary<string, string> data)
{
using var content = new FormUrlEncodedContent(data);
using var responseMessage = await _client.PostAsync(url, content);
responseMessage.EnsureSuccessStatusCode();
return (await responseMessage.Content.ReadFromJsonAsync<T>(_jsonSettings))!;
} }
private static void ValidateResponse<T>([NotNull] T? response,
private static bool ValidateResponse<T>([NotNullWhen(true)] T? response, [CallerArgumentExpression(nameof(response))] string responseName = "") where T : StatusResponse
[CallerArgumentExpression("response")] string responseName = "") where T : StatusResponse
{ {
if (response == null) if (response == null)
{ {
Console.WriteLine(responseName + " failed!"); throw new ResponseFailedException(responseName);
return false;
} }
if (!response.IsSuccess) if (!response.IsSuccess)
{ {
Console.WriteLine(responseName + $" failed with error {response.Status}!"); throw new ResponseFailedException(responseName, response.Status, response.Errors);
if (response.Errors != null) }
}
public void Dispose()
{ {
Console.WriteLine("Errors: " + string.Join(',', response.Errors)); _client.Dispose();
}
return false;
}
return true;
} }
} }

View File

@@ -1,8 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<AssemblyVersion>1.0.0.0</AssemblyVersion> <AnalysisMode>AllEnabledByDefault</AnalysisMode>
<FileVersion>1.0.0.0</FileVersion> <AssemblyVersion>1.1.0</AssemblyVersion>
<FileVersion>1.1.0</FileVersion>
<NoWarn>$(NoWarn);CA1032;CA2007</NoWarn>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
@@ -20,8 +22,31 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CryptSharpStandard" Version="1.0.0"/> <PackageReference Include="CryptSharpStandard" Version="1.0.0"/>
<PackageReference Include="Flurl.Http" Version="4.0.2"/>
<PackageReference Include="NaCl.Net" Version="0.1.13"/> <PackageReference Include="NaCl.Net" Version="0.1.13"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Resources.NeutralResourcesLanguageAttribute">
<_Parameter1>en-US</_Parameter1>
</AssemblyAttribute>
<EmbeddedResource Update="Resources\Localization.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Localization.Designer.cs</LastGenOutput>
</EmbeddedResource>
<Compile Update="Resources\Localization.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Localization.resx</DependentUpon>
</Compile>
<EmbeddedResource Update="Resources\Localization.ru-RU.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Localization.ru-RU.Designer.cs</LastGenOutput>
</EmbeddedResource>
<Compile Update="Resources\Localization.ru-RU.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Localization.ru-RU.resx</DependentUpon>
</Compile>
</ItemGroup>
</Project> </Project>