Compare commits

54 Commits

Author SHA1 Message Date
renovate[bot]
a57d22a993 chore(deps): update actions/download-artifact action to v4.2.1 (#45)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-19 23:05:38 +00:00
renovate[bot]
003c0609e3 chore(deps): update actions/download-artifact action to v4.2.0 (#44)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-18 21:40:37 +00:00
Vita Chumakova
ce705b635c misc: replace flags with language codes 2025-03-17 14:21:11 +04:00
renovate[bot]
b9fa61f7c9 chore(deps): update actions/download-artifact action to v4.1.9 (#43)
* chore(deps): update actions/download-artifact action to v4.1.9

* chore: update SDK in GHA

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Vita Chumakova <me@ezhevita.dev>
2025-02-26 20:50:39 +04:00
Vita Chumakova
84f40a86ac chore: bump version 2024-10-24 16:30:45 +04:00
renovate[bot]
06b7413247 Update actions/checkout digest to 11bd719 2024-10-23 19:48:17 +00:00
renovate[bot]
72b9b1031b Update actions/checkout digest to eef6144 2024-10-07 22:28:53 +00:00
Vita Chumakova
e83a9fbe3d Use JsonTypeInfo 2024-10-03 05:40:28 +04:00
Vita Chumakova
e02654a4d1 Add localization, optimization, better error handling 2024-10-03 05:21:18 +04:00
renovate[bot]
a4778c2883 Update actions/download-artifact action to v4.1.8 2024-07-05 20:25:17 +00:00
renovate[bot]
880c28fc3d Update actions/checkout digest to 692973e 2024-06-13 18:23:02 +00:00
renovate[bot]
88a294554f Update actions/checkout digest to a5ac7e5 2024-05-20 23:08:48 +00:00
renovate[bot]
d4457281e0 Update actions/checkout digest to 44c2b7a 2024-05-08 22:47:01 +00:00
renovate[bot]
9fc4e631c7 Update actions/checkout digest to 0ad4b8f 2024-04-25 18:40:44 +00:00
renovate[bot]
c31900f66f Update actions/download-artifact action to v4.1.7 2024-04-24 18:24:35 +00:00
renovate[bot]
93bb65dc97 Update actions/download-artifact action to v4.1.6 2024-04-22 21:46:50 +00:00
renovate[bot]
608e30ae52 Update actions/checkout digest to 1d96c77 2024-04-22 19:49:24 +00:00
renovate[bot]
744dec7734 Update actions/download-artifact action to v4.1.5 2024-04-18 19:54:39 +00:00
renovate[bot]
2c641811d6 Update actions/download-artifact action to v4.1.4 2024-03-02 22:47:03 +00:00
renovate[bot]
73c551f93f Update actions/download-artifact action to v4.1.3 2024-02-27 01:13:42 +00:00
renovate[bot]
63a2e33f02 Update actions/download-artifact action to v4.1.2 2024-02-06 03:18:58 +00:00
Vita Chumakova
01a2b5e0c4 formatting, improve output messages 2024-01-19 01:10:22 +04:00
renovate[bot]
66dd2db74e Update dependency Flurl.Http to v4 (#24)
* Update dependency Flurl.Http to v4

* fix for flurl

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Vita Chumakova <me@ezhevita.dev>
2024-01-18 18:59:41 +04:00
Vita Chumakova
5aae0c893d update to .net 8 2024-01-18 18:44:00 +04:00
renovate[bot]
532fc0c4aa Update actions/download-artifact action to v4.1.1 2024-01-10 20:07:37 +00:00
renovate[bot]
f065ffa4bd Update actions/download-artifact action to v4.1.0 2023-12-19 06:18:02 +00:00
renovate[bot]
7f1cec556a Update actions/upload-artifact action to v4 (#23)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-15 17:39:35 +04:00
renovate[bot]
4f5f4951b9 Update actions/download-artifact action to v4 (#22)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-15 17:39:24 +04:00
renovate[bot]
671c230bf5 Update actions/setup-dotnet action to v4 (#21)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-05 03:12:45 +04:00
renovate[bot]
a86023d0f6 Update actions/checkout digest to b4ffde6 2023-10-19 15:33:38 +00:00
renovate[bot]
e4466ddcb7 Update actions/checkout digest to 8ade135 2023-09-22 22:44:40 +00:00
renovate[bot]
f439a2fcdf Update actions/checkout action to v4 (#18)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-14 21:32:26 +04:00
renovate[bot]
5ac6a4fe17 Update actions/download-artifact action to v3.0.2 2023-01-06 03:13:17 +00:00
Vita Chumakova
8168ea00ca Merge pull request #16 from ezhevita/renovate/actions-setup-dotnet-3.x
Update actions/setup-dotnet action to v3
2022-11-21 20:29:57 +03:00
Vita Chumakova
14995a5270 Merge pull request #15 from ezhevita/renovate/actions-download-artifact-3.x
Update actions/download-artifact action to v3.0.1
2022-11-21 20:29:42 +03:00
renovate[bot]
9b4fef9bee Update actions/setup-dotnet action to v3 2022-11-20 10:39:29 +00:00
renovate[bot]
9bbfa222b9 Update actions/download-artifact action to v3.0.1 2022-11-20 10:39:23 +00:00
renovate[bot]
3ec53310e4 Update dependency ConfigureAwaitChecker.Analyzer to v5.0.0.1 2022-06-19 02:26:11 +00:00
Vitaliya Chumakova
d136a214f4 Merge pull request #13 from ezhevita/net-6
Use .NET 6
2022-05-22 18:08:27 +03:00
Vitaliya Chumakova
a45245c239 Use .NET 6 2022-05-22 18:06:51 +03:00
Renovate Bot
04287c63f5 Update dependency Flurl.Http to v3.2.4 2022-05-20 21:21:46 +00:00
Renovate Bot
8800716624 Update dependency Flurl.Http to v3.2.3 2022-04-26 01:59:11 +00:00
Vitaliya Chumakova
42987eb113 Merge pull request #10 from Vital7/renovate/actions-upload-artifact-3.x
Update actions/upload-artifact action to v3
2022-03-15 15:53:58 +03:00
Renovate Bot
cbf26e571a Update actions/upload-artifact action to v3 2022-03-03 17:59:55 +00:00
Vitaliya Chumakova
d2ebfd5c33 Merge pull request #9 from Vital7/renovate/actions-download-artifact-3.x
Update actions/download-artifact action to v3
2022-03-03 19:09:02 +03:00
Vitaliya Chumakova
5de4c32ad4 Merge pull request #8 from Vital7/renovate/actions-checkout-3.x
Update actions/checkout action to v3
2022-03-03 19:08:53 +03:00
Vitaliya Chumakova
09ef010610 Merge pull request #7 from Vital7/renovate/actions-setup-dotnet-2.x
Update actions/setup-dotnet action to v2
2022-03-03 19:08:06 +03:00
Renovate Bot
108b39e07d Update actions/download-artifact action to v3 2022-03-02 18:54:18 +00:00
Renovate Bot
637cdae8a1 Update actions/checkout action to v3 2022-03-01 19:54:15 +00:00
Renovate Bot
b66ac260e3 Update actions/setup-dotnet action to v2 2022-02-25 10:44:23 +00:00
Renovate Bot
d72ecd5380 Update dependency Flurl.Http to v3.2.2 2022-01-27 05:15:48 +00:00
Renovate Bot
03c48f8867 Update dependency Flurl.Http to v3.2.1 2022-01-22 23:02:55 +00:00
Renovate Bot
598c5a66b9 Update actions/download-artifact action to v2.1.0 2021-12-07 21:37:50 +00:00
Renovate Bot
3c35854b36 Update dependency Flurl.Serialization.TextJson to v3.1.0 2021-11-15 13:00:57 +00:00
24 changed files with 788 additions and 306 deletions

6
.editorconfig Normal file
View File

@@ -0,0 +1,6 @@
[*.cs]
max_line_length = 130
charset = utf-8
insert_final_newline = true
indent_style = tab
trim_trailing_whitespace = true

View File

@@ -3,28 +3,28 @@ name: .NET
on: [push, pull_request]
env:
DOTNET_SDK_VERSION: 5.0.x
DOTNET_SDK_VERSION: 8.0.x
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Setup .NET
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
- name: Publish generic
run: dotnet publish -c Release -o out/generic -p:UseAppHost=false
run: dotnet publish YandexKeyExtractor/YandexKeyExtractor.csproj -c Release -o out/generic -p:UseAppHost=false
- name: Upload generic artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: ${{ github.event.repository.name }}
path: out/generic
- name: Publish Windows version
run: dotnet publish -c Release -o out/win -p:PublishSingleFile=true -p:PublishTrimmed=true -r win-x64
run: dotnet publish YandexKeyExtractor/YandexKeyExtractor.csproj -c Release -o out/win -p:PublishSingleFile=true -p:PublishTrimmed=true -r win-x64
- name: Upload Windows artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: ${{ github.event.repository.name }}-Windows
path: out/win
@@ -36,13 +36,13 @@ jobs:
steps:
- name: Download generic artifacts
uses: actions/download-artifact@v2.0.10
uses: actions/download-artifact@v4.2.1
with:
name: ${{ github.event.repository.name }}
path: out/${{ github.event.repository.name }}
- name: Download Windows artifacts
uses: actions/download-artifact@v2.0.10
uses: actions/download-artifact@v4.2.1
with:
name: ${{ github.event.repository.name }}-Windows
path: out/${{ github.event.repository.name }}-Windows

2
.gitignore vendored
View File

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

View File

@@ -1,5 +1,5 @@
# 🔑 Yandex.Key Extractor
## 🇺🇸 Extracts TOTP authenticators from [Yandex.Key](https://play.google.com/store/apps/details?id=ru.yandex.key&hl=en&gl=US) app
## [EN] Extracts TOTP authenticators from [Yandex.Key](https://play.google.com/store/apps/details?id=ru.yandex.key&hl=en&gl=US) app
### How to use?
1. Create a cloud backup in the app settings using your device.
@@ -9,7 +9,7 @@
4. TOTP links will be saved in `results.txt`.<br>Use QR code generator to import to another TOTP app or just extract secrets (e.g. for [Bitwarden](https://bitwarden.com/) import).
---
## 🇷🇺 Извлечение двухфакторных аутентификаторов из [Яндекс.Ключ](https://play.google.com/store/apps/details?id=ru.yandex.key&hl=ru&gl=RU)
## [RU] Извлечение двухфакторных аутентификаторов из [Яндекс.Ключ](https://play.google.com/store/apps/details?id=ru.yandex.key&hl=ru&gl=RU)
### Как использовать?
1. Создайте облачную резервную копию в настройках приложения с вашего устройства.

View File

@@ -3,41 +3,74 @@ 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);
namespace YandexKeyExtractor;
ReadOnlySpan<byte> textBytes = Convert.FromBase64String(base64Text).AsSpan();
internal static class Decryptor
{
private const int maxStackallocSize = 4096;
const byte saltLength = 16;
ReadOnlySpan<byte> textData = textBytes[..^saltLength];
ReadOnlySpan<byte> textSalt = textBytes[^saltLength..];
public static string? Decrypt(string encryptedText, string password)
{
var base64Text = NormalizeBase64(encryptedText);
byte[]? generatedPassword = SCrypt.ComputeDerivedKey(Encoding.UTF8.GetBytes(password), textSalt.ToArray(), 32768, 20, 1, null, 32);
var textBytes = Convert.FromBase64String(base64Text);
using XSalsa20Poly1305 secureBox = new(generatedPassword);
const byte SaltLength = 16;
var textData = textBytes.AsSpan()[..^SaltLength];
var salt = textBytes[^SaltLength..];
const byte nonceLength = 24;
ReadOnlySpan<byte> nonce = textData[..nonceLength];
ReadOnlySpan<byte> dataWithMac = textData[nonceLength..];
var generatedPassword = SCrypt.ComputeDerivedKey(
Encoding.UTF8.GetBytes(password),
salt,
cost: 32768,
blockSize: 20,
parallel: 1,
maxThreads: null,
derivedKeyLength: 32
);
using XSalsa20Poly1305 secureBox = new(generatedPassword);
Span<byte> message = dataWithMac.Length <= 4096 ? stackalloc byte[dataWithMac.Length] : new byte[dataWithMac.Length];
const byte NonceLength = 24;
var nonce = textData[..NonceLength];
var dataWithMac = textData[NonceLength..];
const byte macLength = 16;
ReadOnlySpan<byte> data = dataWithMac[macLength..];
ReadOnlySpan<byte> mac = dataWithMac[..macLength];
var message = dataWithMac.Length <= maxStackallocSize
? stackalloc byte[dataWithMac.Length]
: new byte[dataWithMac.Length];
return secureBox.TryDecrypt(message, data, mac, nonce) ? new string(Encoding.UTF8.GetString(message).TrimEnd('\0')) : null;
const byte MacLength = 16;
var data = dataWithMac[MacLength..];
var 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)
{
var suffixLength = (encryptedText.Length % 4) switch
{
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('=');
}
private static string NormalizeBase64(string encryptedText) {
return encryptedText.Replace('-', '+').Replace('_', '/') + (encryptedText.Length % 4) switch {
2 => "==",
3 => "=",
_ => ""
};
}
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

@@ -1,14 +1,9 @@
using System.Text.Json.Serialization;
namespace YandexKeyExtractor.Models {
public class BackupInfoResponse : StatusResponse {
[JsonPropertyName("backup_info")]
public BackupInfo? Info { get; set; }
namespace YandexKeyExtractor.Models;
public class BackupInfo {
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
[JsonPropertyName("updated")]
public uint Updated { get; set; }
}
}
public class BackupInfoResponse : StatusResponse
{
[JsonPropertyName("backup_info")]
public BackupInfo? Info { get; set; }
}

View File

@@ -1,8 +1,9 @@
using System.Text.Json.Serialization;
namespace YandexKeyExtractor.Models {
public class BackupResponse : BackupInfoResponse {
[JsonPropertyName("backup")]
public string? Backup { get; set; }
}
namespace YandexKeyExtractor.Models;
public class BackupResponse : BackupInfoResponse
{
[JsonPropertyName("backup")]
public string? Backup { get; set; }
}

View File

@@ -1,8 +1,10 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace YandexKeyExtractor.Models {
public class CountryResponse : StatusResponse {
[JsonPropertyName("country")]
public string[]? Country { get; set; }
}
namespace YandexKeyExtractor.Models;
public class CountryResponse : StatusResponse
{
[JsonPropertyName("country")]
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

@@ -1,13 +1,9 @@
using System.Text.Json.Serialization;
namespace YandexKeyExtractor.Models {
public class PhoneNumberResponse : StatusResponse {
[JsonPropertyName("number")]
public PhoneNumberInfo? PhoneNumber { get; set; }
namespace YandexKeyExtractor.Models;
public class PhoneNumberInfo {
[JsonPropertyName("e164")]
public string? StandardizedNumber { get; set; }
}
}
public class PhoneNumberResponse : StatusResponse
{
[JsonPropertyName("number")]
public PhoneNumberInfo? PhoneNumber { 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,13 +1,15 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace YandexKeyExtractor.Models {
public class StatusResponse {
[JsonPropertyName("status")]
public string? Status { get; set; }
namespace YandexKeyExtractor.Models;
[JsonPropertyName("errors")]
public string[]? Errors { get; set; }
public class StatusResponse
{
[JsonPropertyName("status")]
public string? Status { get; set; }
public bool IsSuccess => Status == "ok";
}
[JsonPropertyName("errors")]
public IReadOnlyCollection<string>? Errors { get; set; }
public bool IsSuccess => Status == "ok";
}

View File

@@ -1,8 +1,9 @@
using System.Text.Json.Serialization;
namespace YandexKeyExtractor.Models {
public class TrackResponse : StatusResponse {
[JsonPropertyName("track_id")]
public string? TrackID { get; set; }
}
namespace YandexKeyExtractor.Models;
public class TrackResponse : StatusResponse
{
[JsonPropertyName("track_id")]
public string? TrackID { get; set; }
}

View File

@@ -1,65 +1,87 @@
using System;
using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using YandexKeyExtractor;
using YandexKeyExtractor.Exceptions;
namespace YandexKeyExtractor {
internal static class Program {
private static async Task Main() {
Console.WriteLine("Initializing...");
using WebHandler handler = WebHandler.Create();
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("ru-RU");
Console.WriteLine(Localization.Initializing);
using var handler = new WebHandler();
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;
}
string backup;
try
{
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;
} catch (NoValidBackupException)
{
Console.WriteLine(Localization.NoValidBackup);
return;
} catch (Exception e)
{
Console.WriteLine(Localization.UnknownErrorOccurred, e.Message);
throw;
}
PromptInput(out var backupPassword, Localization.BackupPasswordVariableName);
Console.WriteLine(Localization.Decrypting);
var message = Decryptor.Decrypt(backup, backupPassword);
if (string.IsNullOrEmpty(message))
{
Console.WriteLine(Localization.DecryptionFailed);
return;
}
await File.WriteAllTextAsync("result.txt", message);
Console.WriteLine(Localization.Success, message.AsSpan().Count('\n') + 1);
return;
static async Task<string> 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(Localization.PromptVariable, argumentName.ToLower(CultureInfo.CurrentCulture));
var input = Console.ReadLine();
while (string.IsNullOrEmpty(input))
{
Console.WriteLine(Localization.InvalidVariableValue, argumentName);
input = Console.ReadLine();
}
result = input;
}

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,177 +1,132 @@
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.Text.Json.Serialization.Metadata;
using System.Threading.Tasks;
using Flurl.Http;
using Flurl.Serialization.TextJson;
using YandexKeyExtractor.Exceptions;
using YandexKeyExtractor.Models;
namespace YandexKeyExtractor {
public sealed class WebHandler : IDisposable {
private WebHandler(IFlurlClient client) => Client = client;
private IFlurlClient Client { get; }
namespace YandexKeyExtractor;
public void Dispose() {
Client.Dispose();
public sealed class WebHandler : IDisposable
{
private readonly HttpClient _client = new()
{
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)
{
var checkCodeResponse = await PostUrlEncodedAndReceiveJson(
new Uri("bundle/yakey_backup/check_code/", UriKind.Relative),
new Dictionary<string, string>(2) {["code"] = smsCode, ["track_id"] = trackID},
static context => context.StatusResponse);
ValidateResponse(checkCodeResponse);
}
public async Task<string> GetBackupData(string phone, string trackID)
{
var backupResponse = await PostUrlEncodedAndReceiveJson(
new Uri("bundle/yakey_backup/download", UriKind.Relative),
new Dictionary<string, string>(2) {["number"] = phone, ["track_id"] = trackID},
static context => context.BackupResponse);
ValidateResponse(backupResponse);
if (string.IsNullOrEmpty(backupResponse.Backup))
{
throw new NoValidBackupException();
}
public async Task<bool> CheckCode(string? smsCode, string? trackID) {
StatusResponse? checkCodeResponse = await Client.Request("/bundle/yakey_backup/check_code/")
.PostUrlEncodedAsync(
new {
code = smsCode,
track_id = trackID
}
)
.ReceiveJson<StatusResponse?>()
.ConfigureAwait(false);
return backupResponse.Backup;
}
return ValidateResponse(checkCodeResponse, nameof(checkCodeResponse));
public async Task<string> GetPhoneNumberInfo(string phoneNumber, string country)
{
var phoneNumberResponse = await PostUrlEncodedAndReceiveJson<PhoneNumberResponse>(
new Uri("bundle/validate/phone_number/", UriKind.Relative),
new Dictionary<string, string>(2) {["phone_number"] = phoneNumber, ["country"] = country},
static context => context.PhoneNumberResponse);
var phone = phoneNumberResponse?.PhoneNumber?.StandardizedNumber ?? $"+{phoneNumber}";
return phone;
}
public async Task<string> SendSMSCodeAndGetTrackID(string phone, string country)
{
var trackResponse = await PostUrlEncodedAndReceiveJson(
new Uri("bundle/yakey_backup/send_code/", UriKind.Relative),
new Dictionary<string, string>(3) {["display_language"] = "en", ["number"] = phone, ["country"] = country},
static context => context.TrackResponse);
ValidateResponse(trackResponse);
var trackID = trackResponse.TrackID;
if (string.IsNullOrEmpty(trackID))
{
throw new InvalidTrackIdException();
}
public static WebHandler Create() {
JsonSerializerOptions jsonSettings = new() {
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
return trackID;
}
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));
public async Task<string?> TryGetCountry()
{
var countryResponse = await _client.GetFromJsonAsync(
new Uri("suggest/country", UriKind.Relative), SourceGenerationContext.Default.CountryResponse);
return new WebHandler(client);
}
return countryResponse?.Country?.FirstOrDefault();
}
public async Task<string?> GetBackupData(string phone, string? trackID) {
BackupResponse? backupResponse = await Client.Request("/bundle/yakey_backup/download")
.PostUrlEncodedAsync(
new {
number = phone,
track_id = trackID
}
)
.ReceiveJson<BackupResponse?>()
.ConfigureAwait(false);
public async Task ValidateBackupInfo(string phone, string trackID, string country)
{
var backupInfoResponse = await PostUrlEncodedAndReceiveJson(
new Uri("bundle/yakey_backup/info/", UriKind.Relative),
new Dictionary<string, string>(3) {["number"] = phone, ["track_id"] = trackID, ["country"] = country},
static context => context.BackupInfoResponse);
if (!ValidateResponse(backupResponse, nameof(backupResponse))) {
return null;
}
ValidateResponse(backupInfoResponse);
if (string.IsNullOrEmpty(backupResponse.Backup)) {
Console.WriteLine("Fatal error - Couldn't find valid backup!");
return null;
}
return backupResponse.Backup;
}
public async Task<string> GetPhoneNumberInfo(string? phoneNumber, string country) {
PhoneNumberResponse? phoneNumberResponse = await Client.Request("/bundle/validate/phone_number/")
.PostUrlEncodedAsync(
new {
phone_number = phoneNumber,
country
}
).ReceiveJson<PhoneNumberResponse?>()
.ConfigureAwait(false);
ValidateResponse(phoneNumberResponse, nameof(phoneNumberResponse));
string phone = phoneNumberResponse?.PhoneNumber?.StandardizedNumber ?? '+' + phoneNumber;
return phone;
}
public async Task<string?> SendSMSCodeAndGetTrackID(string phone, string country) {
TrackResponse? trackResponse = await Client.Request("/bundle/yakey_backup/send_code/")
.PostUrlEncodedAsync(
new {
display_language = "en",
number = phone,
country
}
)
.ReceiveJson<TrackResponse?>()
.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<string> TryGetCountry() {
CountryResponse? countryResponse = await Client.Request("/suggest/country")
.GetAsync()
.ReceiveJson<CountryResponse?>()
.ConfigureAwait(false);
ValidateResponse(countryResponse, nameof(countryResponse));
string country = countryResponse?.Country?.FirstOrDefault() ?? "ru";
return country;
}
public async Task<bool> 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<BackupInfoResponse?>()
.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<T>([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;
if (backupInfoResponse.Info?.Updated == null)
{
throw new NoValidBackupException();
}
}
private async Task<T?> PostUrlEncodedAndReceiveJson<T>(Uri url, Dictionary<string, string> data,
Func<SourceGenerationContext, JsonTypeInfo<T>> typeInfoProvider)
{
using var content = new FormUrlEncodedContent(data);
using var responseMessage = await _client.PostAsync(url, content);
responseMessage.EnsureSuccessStatusCode();
return (await responseMessage.Content.ReadFromJsonAsync(typeInfoProvider(SourceGenerationContext.Default)))!;
}
private static void ValidateResponse<T>([NotNull] T? response,
[CallerArgumentExpression(nameof(response))] string responseName = "") where T : StatusResponse
{
if (response == null)
{
throw new ResponseFailedException(responseName);
}
if (!response.IsSuccess)
{
throw new ResponseFailedException(responseName, response.Status, response.Errors);
}
}
public void Dispose()
{
_client.Dispose();
}
}

View File

@@ -1,11 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<AssemblyVersion>1.1.1</AssemblyVersion>
<FileVersion>1.1.1</FileVersion>
<NoWarn>$(NoWarn);CA1032;CA2007</NoWarn>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<!-- Trimming features -->
@@ -19,14 +21,32 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="CryptSharpStandard" Version="1.0.0" />
<PackageReference Include="Flurl.Http" Version="3.2.0" />
<PackageReference Include="Flurl.Serialization.TextJson" Version="3.0.0" />
<PackageReference Include="NaCl.Net" Version="0.1.13" />
<PackageReference Include="CryptSharpStandard" Version="1.0.0"/>
<PackageReference Include="NaCl.Net" Version="0.1.13"/>
</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>