From f942d580824a76bca086988a9cba373d7a444084 Mon Sep 17 00:00:00 2001 From: Vitaliya Chumakova Date: Thu, 5 Aug 2021 22:30:12 +0300 Subject: [PATCH] Initial commit --- .gitignore | 3 + YandexKeyExtractor.sln | 16 ++ YandexKeyExtractor/Decryptor.cs | 42 +++++ .../Models/BackupInfoResponse.cs | 14 ++ YandexKeyExtractor/Models/BackupResponse.cs | 8 + YandexKeyExtractor/Models/CountryResponse.cs | 8 + .../Models/PhoneNumberResponse.cs | 13 ++ YandexKeyExtractor/Models/StatusResponse.cs | 13 ++ YandexKeyExtractor/Models/TrackResponse.cs | 8 + YandexKeyExtractor/Program.cs | 65 +++++++ YandexKeyExtractor/WebHandler.cs | 177 ++++++++++++++++++ YandexKeyExtractor/YandexKeyExtractor.csproj | 21 +++ 12 files changed, 388 insertions(+) create mode 100644 .gitignore create mode 100644 YandexKeyExtractor.sln create mode 100644 YandexKeyExtractor/Decryptor.cs create mode 100644 YandexKeyExtractor/Models/BackupInfoResponse.cs create mode 100644 YandexKeyExtractor/Models/BackupResponse.cs create mode 100644 YandexKeyExtractor/Models/CountryResponse.cs create mode 100644 YandexKeyExtractor/Models/PhoneNumberResponse.cs create mode 100644 YandexKeyExtractor/Models/StatusResponse.cs create mode 100644 YandexKeyExtractor/Models/TrackResponse.cs create mode 100644 YandexKeyExtractor/Program.cs create mode 100644 YandexKeyExtractor/WebHandler.cs create mode 100644 YandexKeyExtractor/YandexKeyExtractor.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..487ea2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +.idea \ No newline at end of file diff --git a/YandexKeyExtractor.sln b/YandexKeyExtractor.sln new file mode 100644 index 0000000..1c5429f --- /dev/null +++ b/YandexKeyExtractor.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YandexKeyExtractor", "YandexKeyExtractor\YandexKeyExtractor.csproj", "{D059A2F0-8723-4479-89AE-59BC5F09B27D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D059A2F0-8723-4479-89AE-59BC5F09B27D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D059A2F0-8723-4479-89AE-59BC5F09B27D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D059A2F0-8723-4479-89AE-59BC5F09B27D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D059A2F0-8723-4479-89AE-59BC5F09B27D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/YandexKeyExtractor/Decryptor.cs b/YandexKeyExtractor/Decryptor.cs new file mode 100644 index 0000000..95742d1 --- /dev/null +++ b/YandexKeyExtractor/Decryptor.cs @@ -0,0 +1,42 @@ +using System; +using System.Text; +using CryptSharp.Utility; +using NaCl; + +namespace YandexKeyExtractor { + public static class Decryptor { + public static string? Decrypt(string encryptedText, string password) { + string base64Text = NormalizeBase64(encryptedText); + + Span textBytes = Convert.FromBase64String(base64Text).AsSpan(); + + const byte saltLength = 16; + Span textData = textBytes[..^saltLength]; + Span textSalt = textBytes[^saltLength..]; + + byte[]? generatedPassword = SCrypt.ComputeDerivedKey(Encoding.UTF8.GetBytes(password), textSalt.ToArray(), 32768, 20, 1, null, 32); + + using XSalsa20Poly1305 secureBox = new(generatedPassword); + + const byte nonceLength = 24; + Span nonce = textData[..nonceLength]; + Span dataWithMac = textData[nonceLength..]; + + Span message = stackalloc byte[dataWithMac.Length]; + + const byte macLength = 16; + Span data = dataWithMac[macLength..]; + Span mac = dataWithMac[..macLength]; + + return secureBox.TryDecrypt(message, data, mac, nonce) ? new string(Encoding.UTF8.GetString(message).TrimEnd('\0')) : null; + } + + private static string NormalizeBase64(string encryptedText) { + return encryptedText.Replace('-', '+').Replace('_', '/') + (encryptedText.Length % 4) switch { + 2 => "==", + 3 => "=", + _ => "" + }; + } + } +} diff --git a/YandexKeyExtractor/Models/BackupInfoResponse.cs b/YandexKeyExtractor/Models/BackupInfoResponse.cs new file mode 100644 index 0000000..148cd54 --- /dev/null +++ b/YandexKeyExtractor/Models/BackupInfoResponse.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace YandexKeyExtractor.Models { + 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/BackupResponse.cs b/YandexKeyExtractor/Models/BackupResponse.cs new file mode 100644 index 0000000..3f767f3 --- /dev/null +++ b/YandexKeyExtractor/Models/BackupResponse.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace YandexKeyExtractor.Models { + public class BackupResponse : BackupInfoResponse { + [JsonPropertyName("backup")] + public string? Backup { get; set; } + } +} diff --git a/YandexKeyExtractor/Models/CountryResponse.cs b/YandexKeyExtractor/Models/CountryResponse.cs new file mode 100644 index 0000000..fec9aad --- /dev/null +++ b/YandexKeyExtractor/Models/CountryResponse.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace YandexKeyExtractor.Models { + public class CountryResponse : StatusResponse { + [JsonPropertyName("country")] + public string[]? Country { get; set; } + } +} diff --git a/YandexKeyExtractor/Models/PhoneNumberResponse.cs b/YandexKeyExtractor/Models/PhoneNumberResponse.cs new file mode 100644 index 0000000..7005ab4 --- /dev/null +++ b/YandexKeyExtractor/Models/PhoneNumberResponse.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace YandexKeyExtractor.Models { + 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/StatusResponse.cs b/YandexKeyExtractor/Models/StatusResponse.cs new file mode 100644 index 0000000..1887fc2 --- /dev/null +++ b/YandexKeyExtractor/Models/StatusResponse.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace YandexKeyExtractor.Models { + public class StatusResponse { + [JsonPropertyName("status")] + public string? Status { get; set; } + + [JsonPropertyName("errors")] + public string[]? Errors { get; set; } + + public bool IsSuccess => Status == "ok"; + } +} diff --git a/YandexKeyExtractor/Models/TrackResponse.cs b/YandexKeyExtractor/Models/TrackResponse.cs new file mode 100644 index 0000000..8e3e1c0 --- /dev/null +++ b/YandexKeyExtractor/Models/TrackResponse.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace YandexKeyExtractor.Models { + public class TrackResponse : StatusResponse { + [JsonPropertyName("track_id")] + public string? TrackID { get; set; } + } +} diff --git a/YandexKeyExtractor/Program.cs b/YandexKeyExtractor/Program.cs new file mode 100644 index 0000000..2350da0 --- /dev/null +++ b/YandexKeyExtractor/Program.cs @@ -0,0 +1,65 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace YandexKeyExtractor { + internal static class Program { + private static async Task Main() { + Console.WriteLine("Initializing..."); + using WebHandler handler = WebHandler.Create(); + + string country = await handler.TryGetCountry().ConfigureAwait(false); + + PromptInput(out string phoneNumber, nameof(phoneNumber)); + + phoneNumber = phoneNumber.TrimStart('+'); + string phone = await handler.GetPhoneNumberInfo(phoneNumber, country).ConfigureAwait(false); + + string? trackID = await handler.SendSMSCodeAndGetTrackID(phone, country).ConfigureAwait(false); + if (string.IsNullOrEmpty(trackID)) { + return; + } + + PromptInput(out string smsCode, nameof(smsCode)); + + if (!await handler.CheckCode(smsCode, trackID).ConfigureAwait(false)) { + return; + } + + if (!await handler.ValidateBackupInfo(phone, trackID, country).ConfigureAwait(false)) { + return; + } + + string? backup = await handler.GetBackupData(phone, trackID).ConfigureAwait(false); + if (string.IsNullOrEmpty(backup)) { + return; + } + + PromptInput(out string backupPassword, nameof(backupPassword)); + + Console.WriteLine("Decrypting..."); + string? message = Decryptor.Decrypt(backup, backupPassword); + if (string.IsNullOrEmpty(message)) { + Console.WriteLine("Decryption failed!"); + + return; + } + + Console.WriteLine("Successfully decrypted!"); + await File.WriteAllTextAsync("result.txt", message).ConfigureAwait(false); + Console.WriteLine($"Written {message.Split('\n').Length} authenticators to result file"); + } + + private static void PromptInput(out string result, [CallerArgumentExpression("result")] string argumentName = "") { + Console.WriteLine($"Enter {argumentName}:"); + string? input = Console.ReadLine(); + while (string.IsNullOrEmpty(input)) { + Console.WriteLine($"{argumentName} is invalid, try again:"); + input = Console.ReadLine(); + } + + result = input; + } + } +} diff --git a/YandexKeyExtractor/WebHandler.cs b/YandexKeyExtractor/WebHandler.cs new file mode 100644 index 0000000..c70c21e --- /dev/null +++ b/YandexKeyExtractor/WebHandler.cs @@ -0,0 +1,177 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Flurl.Http; +using Flurl.Serialization.TextJson; +using YandexKeyExtractor.Models; + +namespace YandexKeyExtractor { + public sealed class WebHandler : IDisposable { + private WebHandler(IFlurlClient client) => Client = client; + private IFlurlClient Client { get; } + + public void Dispose() { + Client.Dispose(); + } + + public async Task CheckCode(string? smsCode, string? trackID) { + StatusResponse? checkCodeResponse = await Client.Request("/bundle/yakey_backup/check_code/") + .PostUrlEncodedAsync( + new { + code = smsCode, + track_id = trackID + } + ) + .ReceiveJson() + .ConfigureAwait(false); + + return ValidateResponse(checkCodeResponse, nameof(checkCodeResponse)); + } + + public static WebHandler Create() { + JsonSerializerOptions jsonSettings = new() { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + IFlurlClient? client = new FlurlClient( + new HttpClient { + DefaultRequestHeaders = { UserAgent = { new ProductInfoHeaderValue("okhttp", "2.7.5") } }, + BaseAddress = new Uri("https://registrator.mobile.yandex.net/1/") + } + ).Configure(settings => settings.WithTextJsonSerializer(jsonSettings)); + + return new WebHandler(client); + } + + public async Task GetBackupData(string phone, string? trackID) { + BackupResponse? backupResponse = await Client.Request("/bundle/yakey_backup/download") + .PostUrlEncodedAsync( + new { + number = phone, + track_id = trackID + } + ) + .ReceiveJson() + .ConfigureAwait(false); + + if (!ValidateResponse(backupResponse, nameof(backupResponse))) { + return null; + } + + if (string.IsNullOrEmpty(backupResponse.Backup)) { + Console.WriteLine("Fatal error - Couldn't find valid backup!"); + + return null; + } + + return backupResponse.Backup; + } + + public async Task GetPhoneNumberInfo(string? phoneNumber, string country) { + PhoneNumberResponse? phoneNumberResponse = await Client.Request("/bundle/validate/phone_number/") + .PostUrlEncodedAsync( + new { + phone_number = phoneNumber, + country + } + ).ReceiveJson() + .ConfigureAwait(false); + + ValidateResponse(phoneNumberResponse, nameof(phoneNumberResponse)); + + string phone = phoneNumberResponse?.PhoneNumber?.StandardizedNumber ?? '+' + phoneNumber; + + return phone; + } + + public async Task SendSMSCodeAndGetTrackID(string phone, string country) { + TrackResponse? trackResponse = await Client.Request("/bundle/yakey_backup/send_code/") + .PostUrlEncodedAsync( + new { + display_language = "en", + number = phone, + country + } + ) + .ReceiveJson() + .ConfigureAwait(false); + + if (!ValidateResponse(trackResponse, nameof(trackResponse))) { + return null; + } + + string? trackID = trackResponse.TrackID; + if (string.IsNullOrEmpty(trackID)) { + Console.WriteLine("Track ID is empty!"); + + return null; + } + + return trackID; + } + + public async Task TryGetCountry() { + CountryResponse? countryResponse = await Client.Request("/suggest/country") + .GetAsync() + .ReceiveJson() + .ConfigureAwait(false); + + ValidateResponse(countryResponse, nameof(countryResponse)); + + string country = countryResponse?.Country?.FirstOrDefault() ?? "ru"; + + return country; + } + + public async Task ValidateBackupInfo(string phone, string? trackID, string country) { + BackupInfoResponse? backupInfoResponse = await Client.Request("/bundle/yakey_backup/info/") + .PostUrlEncodedAsync( + new { + number = phone, + track_id = trackID, + country + } + ) + .ReceiveJson() + .ConfigureAwait(false); + + if (!ValidateResponse(backupInfoResponse, nameof(backupInfoResponse))) { + return false; + } + + if (backupInfoResponse.Info?.Updated == null) { + Console.WriteLine("Fatal error - Couldn't find valid backup!"); + + return false; + } + + return true; + } + + + private static bool ValidateResponse([NotNullWhen(true)] T? response, [CallerArgumentExpression("response")] string responseName = "") where T : StatusResponse { + if (response == null) { + Console.WriteLine(responseName + " failed!"); + + return false; + } + + if (!response.IsSuccess) { + Console.WriteLine(responseName + $" failed with error {response.Status}!"); + if (response.Errors != null) { + Console.WriteLine("Errors: " + string.Join(',', response.Errors)); + } + + return false; + } + + return true; + } + } +} diff --git a/YandexKeyExtractor/YandexKeyExtractor.csproj b/YandexKeyExtractor/YandexKeyExtractor.csproj new file mode 100644 index 0000000..943dee3 --- /dev/null +++ b/YandexKeyExtractor/YandexKeyExtractor.csproj @@ -0,0 +1,21 @@ + + + + Exe + preview + net6.0 + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + +