mirror of
https://github.com/ezhevita/YandexKeyExtractor
synced 2025-08-16 19:40:48 +07:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01b7d30754 | ||
|
|
40b0f2f582 | ||
|
|
5c3c2a9503 | ||
|
|
fd891dd613 | ||
|
|
a57d22a993 | ||
|
|
003c0609e3 | ||
|
|
ce705b635c | ||
|
|
b9fa61f7c9 | ||
|
|
84f40a86ac | ||
|
|
06b7413247 | ||
|
|
72b9b1031b | ||
|
|
e83a9fbe3d | ||
|
|
e02654a4d1 | ||
|
|
a4778c2883 | ||
|
|
880c28fc3d | ||
|
|
88a294554f | ||
|
|
d4457281e0 | ||
|
|
9fc4e631c7 | ||
|
|
c31900f66f | ||
|
|
93bb65dc97 | ||
|
|
608e30ae52 | ||
|
|
744dec7734 | ||
|
|
2c641811d6 | ||
|
|
73c551f93f | ||
|
|
63a2e33f02 | ||
|
|
01a2b5e0c4 | ||
|
|
66dd2db74e | ||
|
|
5aae0c893d | ||
|
|
532fc0c4aa | ||
|
|
f065ffa4bd | ||
|
|
7f1cec556a | ||
|
|
4f5f4951b9 | ||
|
|
671c230bf5 | ||
|
|
a86023d0f6 | ||
|
|
e4466ddcb7 | ||
|
|
f439a2fcdf | ||
|
|
5ac6a4fe17 | ||
|
|
8168ea00ca | ||
|
|
14995a5270 | ||
|
|
9b4fef9bee | ||
|
|
9bbfa222b9 | ||
|
|
3ec53310e4 | ||
|
|
d136a214f4 | ||
|
|
a45245c239 | ||
|
|
04287c63f5 | ||
|
|
8800716624 | ||
|
|
42987eb113 | ||
|
|
cbf26e571a | ||
|
|
d2ebfd5c33 | ||
|
|
5de4c32ad4 | ||
|
|
09ef010610 | ||
|
|
108b39e07d | ||
|
|
637cdae8a1 | ||
|
|
b66ac260e3 | ||
|
|
d72ecd5380 | ||
|
|
03c48f8867 | ||
|
|
598c5a66b9 | ||
|
|
3c35854b36 |
6
.editorconfig
Normal file
6
.editorconfig
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[*.cs]
|
||||||
|
max_line_length = 130
|
||||||
|
charset = utf-8
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = tab
|
||||||
|
trim_trailing_whitespace = true
|
||||||
18
.github/workflows/dotnet.yml
vendored
18
.github/workflows/dotnet.yml
vendored
@@ -3,28 +3,28 @@ name: .NET
|
|||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DOTNET_SDK_VERSION: 5.0.x
|
DOTNET_SDK_VERSION: 8.0.x
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v1
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
|
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
|
||||||
- name: Publish generic
|
- 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
|
- name: Upload generic artifacts
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ github.event.repository.name }}
|
name: ${{ github.event.repository.name }}
|
||||||
path: out/generic
|
path: out/generic
|
||||||
- name: Publish Windows version
|
- 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
|
- name: Upload Windows artifacts
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ github.event.repository.name }}-Windows
|
name: ${{ github.event.repository.name }}-Windows
|
||||||
path: out/win
|
path: out/win
|
||||||
@@ -36,13 +36,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download generic artifacts
|
- name: Download generic artifacts
|
||||||
uses: actions/download-artifact@v2.0.10
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: ${{ github.event.repository.name }}
|
name: ${{ github.event.repository.name }}
|
||||||
path: out/${{ github.event.repository.name }}
|
path: out/${{ github.event.repository.name }}
|
||||||
|
|
||||||
- name: Download Windows artifacts
|
- name: Download Windows artifacts
|
||||||
uses: actions/download-artifact@v2.0.10
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: ${{ github.event.repository.name }}-Windows
|
name: ${{ github.event.repository.name }}-Windows
|
||||||
path: out/${{ github.event.repository.name }}-Windows
|
path: out/${{ github.event.repository.name }}-Windows
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
.idea
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.DotSettings.user
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# 🔑 Yandex.Key Extractor
|
# 🔑 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?
|
### How to use?
|
||||||
1. Create a cloud backup in the app settings using your device.
|
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).
|
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. Создайте облачную резервную копию в настройках приложения с вашего устройства.
|
1. Создайте облачную резервную копию в настройках приложения с вашего устройства.
|
||||||
|
|||||||
@@ -3,41 +3,74 @@ using System.Text;
|
|||||||
using CryptSharp.Utility;
|
using CryptSharp.Utility;
|
||||||
using NaCl;
|
using NaCl;
|
||||||
|
|
||||||
namespace YandexKeyExtractor {
|
namespace YandexKeyExtractor;
|
||||||
public static class Decryptor {
|
|
||||||
public static string? Decrypt(string encryptedText, string password) {
|
|
||||||
string base64Text = NormalizeBase64(encryptedText);
|
|
||||||
|
|
||||||
ReadOnlySpan<byte> textBytes = Convert.FromBase64String(base64Text).AsSpan();
|
internal static class Decryptor
|
||||||
|
{
|
||||||
|
private const int maxStackallocSize = 4096;
|
||||||
|
|
||||||
const byte saltLength = 16;
|
public static string? Decrypt(string encryptedText, string password)
|
||||||
ReadOnlySpan<byte> textData = textBytes[..^saltLength];
|
{
|
||||||
ReadOnlySpan<byte> textSalt = textBytes[^saltLength..];
|
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;
|
var generatedPassword = SCrypt.ComputeDerivedKey(
|
||||||
ReadOnlySpan<byte> nonce = textData[..nonceLength];
|
Encoding.UTF8.GetBytes(password),
|
||||||
ReadOnlySpan<byte> dataWithMac = textData[nonceLength..];
|
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;
|
var message = dataWithMac.Length <= maxStackallocSize
|
||||||
ReadOnlySpan<byte> data = dataWithMac[macLength..];
|
? stackalloc byte[dataWithMac.Length]
|
||||||
ReadOnlySpan<byte> mac = dataWithMac[..macLength];
|
: 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 new string(normalized);
|
||||||
return encryptedText.Replace('-', '+').Replace('_', '/') + (encryptedText.Length % 4) switch {
|
|
||||||
2 => "==",
|
|
||||||
3 => "=",
|
|
||||||
_ => ""
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
YandexKeyExtractor/Exceptions/InvalidTrackIdException.cs
Normal file
10
YandexKeyExtractor/Exceptions/InvalidTrackIdException.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace YandexKeyExtractor.Exceptions;
|
||||||
|
|
||||||
|
public class InvalidTrackIdException : Exception
|
||||||
|
{
|
||||||
|
public InvalidTrackIdException() : base("Invalid track ID.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
10
YandexKeyExtractor/Exceptions/NoValidBackupException.cs
Normal file
10
YandexKeyExtractor/Exceptions/NoValidBackupException.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace YandexKeyExtractor.Exceptions;
|
||||||
|
|
||||||
|
public class NoValidBackupException : Exception
|
||||||
|
{
|
||||||
|
public NoValidBackupException() : base(Localization.NoValidBackup)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
22
YandexKeyExtractor/Exceptions/ResponseFailedException.cs
Normal file
22
YandexKeyExtractor/Exceptions/ResponseFailedException.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
YandexKeyExtractor/Models/BackupInfo.cs
Normal file
10
YandexKeyExtractor/Models/BackupInfo.cs
Normal 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; }
|
||||||
|
}
|
||||||
@@ -1,14 +1,9 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace YandexKeyExtractor.Models {
|
namespace YandexKeyExtractor.Models;
|
||||||
public class BackupInfoResponse : StatusResponse {
|
|
||||||
[JsonPropertyName("backup_info")]
|
|
||||||
public BackupInfo? Info { get; set; }
|
|
||||||
|
|
||||||
public class BackupInfo {
|
public class BackupInfoResponse : StatusResponse
|
||||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
|
{
|
||||||
[JsonPropertyName("updated")]
|
[JsonPropertyName("backup_info")]
|
||||||
public uint Updated { get; set; }
|
public BackupInfo? Info { get; set; }
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace YandexKeyExtractor.Models {
|
namespace YandexKeyExtractor.Models;
|
||||||
public class BackupResponse : BackupInfoResponse {
|
|
||||||
[JsonPropertyName("backup")]
|
public class BackupResponse : BackupInfoResponse
|
||||||
public string? Backup { get; set; }
|
{
|
||||||
}
|
[JsonPropertyName("backup")]
|
||||||
|
public string? Backup { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace YandexKeyExtractor.Models {
|
namespace YandexKeyExtractor.Models;
|
||||||
public class CountryResponse : StatusResponse {
|
|
||||||
[JsonPropertyName("country")]
|
public class CountryResponse : StatusResponse
|
||||||
public string[]? Country { get; set; }
|
{
|
||||||
}
|
[JsonPropertyName("country")]
|
||||||
|
public IReadOnlyCollection<string>? Country { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
9
YandexKeyExtractor/Models/PhoneNumberInfo.cs
Normal file
9
YandexKeyExtractor/Models/PhoneNumberInfo.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace YandexKeyExtractor.Models;
|
||||||
|
|
||||||
|
public class PhoneNumberInfo
|
||||||
|
{
|
||||||
|
[JsonPropertyName("e164")]
|
||||||
|
public string? StandardizedNumber { get; set; }
|
||||||
|
}
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace YandexKeyExtractor.Models {
|
namespace YandexKeyExtractor.Models;
|
||||||
public class PhoneNumberResponse : StatusResponse {
|
|
||||||
[JsonPropertyName("number")]
|
|
||||||
public PhoneNumberInfo? PhoneNumber { get; set; }
|
|
||||||
|
|
||||||
public class PhoneNumberInfo {
|
public class PhoneNumberResponse : StatusResponse
|
||||||
[JsonPropertyName("e164")]
|
{
|
||||||
public string? StandardizedNumber { get; set; }
|
[JsonPropertyName("number")]
|
||||||
}
|
public PhoneNumberInfo? PhoneNumber { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
14
YandexKeyExtractor/Models/SourceGenerationContext.cs
Normal file
14
YandexKeyExtractor/Models/SourceGenerationContext.cs
Normal 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
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace YandexKeyExtractor.Models {
|
namespace YandexKeyExtractor.Models;
|
||||||
public class StatusResponse {
|
|
||||||
[JsonPropertyName("status")]
|
|
||||||
public string? Status { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("errors")]
|
public class StatusResponse
|
||||||
public string[]? Errors { get; set; }
|
{
|
||||||
|
[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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace YandexKeyExtractor.Models {
|
namespace YandexKeyExtractor.Models;
|
||||||
public class TrackResponse : StatusResponse {
|
|
||||||
[JsonPropertyName("track_id")]
|
public class TrackResponse : StatusResponse
|
||||||
public string? TrackID { get; set; }
|
{
|
||||||
}
|
[JsonPropertyName("track_id")]
|
||||||
|
public string? TrackID { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,87 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using YandexKeyExtractor;
|
||||||
|
using YandexKeyExtractor.Exceptions;
|
||||||
|
|
||||||
namespace YandexKeyExtractor {
|
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("ru-RU");
|
||||||
internal static class Program {
|
Console.WriteLine(Localization.Initializing);
|
||||||
private static async Task Main() {
|
using var handler = new WebHandler();
|
||||||
Console.WriteLine("Initializing...");
|
|
||||||
using WebHandler handler = WebHandler.Create();
|
|
||||||
|
|
||||||
string country = await handler.TryGetCountry().ConfigureAwait(false);
|
string backup;
|
||||||
|
try
|
||||||
PromptInput(out string phoneNumber, nameof(phoneNumber));
|
{
|
||||||
|
backup = await RetrieveBackup(handler);
|
||||||
phoneNumber = phoneNumber.TrimStart('+');
|
} catch (ResponseFailedException e)
|
||||||
string phone = await handler.GetPhoneNumberInfo(phoneNumber, country).ConfigureAwait(false);
|
{
|
||||||
|
if (e.Status == null)
|
||||||
string? trackID = await handler.SendSMSCodeAndGetTrackID(phone, country).ConfigureAwait(false);
|
{
|
||||||
if (string.IsNullOrEmpty(trackID)) {
|
Console.WriteLine(Localization.ResponseFailed, e.ResponseName);
|
||||||
return;
|
} else
|
||||||
}
|
{
|
||||||
|
Console.WriteLine(Localization.ResponseFailedWithDetails, e.Status, e.ResponseName, string.Join(", ", e.Errors ?? []));
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
126
YandexKeyExtractor/Resources/Localization.Designer.cs
generated
Normal file
126
YandexKeyExtractor/Resources/Localization.Designer.cs
generated
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
YandexKeyExtractor/Resources/Localization.resx
Normal file
60
YandexKeyExtractor/Resources/Localization.resx
Normal 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>
|
||||||
126
YandexKeyExtractor/Resources/Localization.ru-RU.Designer.cs
generated
Normal file
126
YandexKeyExtractor/Resources/Localization.ru-RU.Designer.cs
generated
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
YandexKeyExtractor/Resources/Localization.ru-RU.resx
Normal file
60
YandexKeyExtractor/Resources/Localization.ru-RU.resx
Normal 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>
|
||||||
@@ -1,177 +1,132 @@
|
|||||||
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.Serialization.Metadata;
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Flurl.Http;
|
using YandexKeyExtractor.Exceptions;
|
||||||
using Flurl.Serialization.TextJson;
|
|
||||||
using YandexKeyExtractor.Models;
|
using YandexKeyExtractor.Models;
|
||||||
|
|
||||||
namespace YandexKeyExtractor {
|
namespace YandexKeyExtractor;
|
||||||
public sealed class WebHandler : IDisposable {
|
|
||||||
private WebHandler(IFlurlClient client) => Client = client;
|
|
||||||
private IFlurlClient Client { get; }
|
|
||||||
|
|
||||||
public void Dispose() {
|
public sealed class WebHandler : IDisposable
|
||||||
Client.Dispose();
|
{
|
||||||
|
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) {
|
return backupResponse.Backup;
|
||||||
StatusResponse? checkCodeResponse = await Client.Request("/bundle/yakey_backup/check_code/")
|
}
|
||||||
.PostUrlEncodedAsync(
|
|
||||||
new {
|
|
||||||
code = smsCode,
|
|
||||||
track_id = trackID
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.ReceiveJson<StatusResponse?>()
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
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() {
|
return trackID;
|
||||||
JsonSerializerOptions jsonSettings = new() {
|
}
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
|
||||||
};
|
|
||||||
|
|
||||||
IFlurlClient? client = new FlurlClient(
|
public async Task<string?> TryGetCountry()
|
||||||
new HttpClient {
|
{
|
||||||
DefaultRequestHeaders = { UserAgent = { new ProductInfoHeaderValue("okhttp", "2.7.5") } },
|
var countryResponse = await _client.GetFromJsonAsync(
|
||||||
BaseAddress = new Uri("https://registrator.mobile.yandex.net/1/")
|
new Uri("suggest/country", UriKind.Relative), SourceGenerationContext.Default.CountryResponse);
|
||||||
}
|
|
||||||
).Configure(settings => settings.WithTextJsonSerializer(jsonSettings));
|
|
||||||
|
|
||||||
return new WebHandler(client);
|
return countryResponse?.Country?.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string?> GetBackupData(string phone, string? trackID) {
|
public async Task ValidateBackupInfo(string phone, string trackID, string country)
|
||||||
BackupResponse? backupResponse = await Client.Request("/bundle/yakey_backup/download")
|
{
|
||||||
.PostUrlEncodedAsync(
|
var backupInfoResponse = await PostUrlEncodedAndReceiveJson(
|
||||||
new {
|
new Uri("bundle/yakey_backup/info/", UriKind.Relative),
|
||||||
number = phone,
|
new Dictionary<string, string>(3) {["number"] = phone, ["track_id"] = trackID, ["country"] = country},
|
||||||
track_id = trackID
|
static context => context.BackupInfoResponse);
|
||||||
}
|
|
||||||
)
|
|
||||||
.ReceiveJson<BackupResponse?>()
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!ValidateResponse(backupResponse, nameof(backupResponse))) {
|
ValidateResponse(backupInfoResponse);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(backupResponse.Backup)) {
|
if (backupInfoResponse.Info?.Updated == null)
|
||||||
Console.WriteLine("Fatal error - Couldn't find valid backup!");
|
{
|
||||||
|
throw new NoValidBackupException();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<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.1</AssemblyVersion>
|
||||||
|
<FileVersion>1.1.1</FileVersion>
|
||||||
|
<NoWarn>$(NoWarn);CA1032;CA2007</NoWarn>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Trimming features -->
|
<!-- Trimming features -->
|
||||||
@@ -19,14 +21,32 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0">
|
<PackageReference Include="CryptSharpStandard" Version="1.0.0"/>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PackageReference Include="NaCl.Net" Version="0.1.13"/>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
</ItemGroup>
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="CryptSharpStandard" Version="1.0.0" />
|
<ItemGroup>
|
||||||
<PackageReference Include="Flurl.Http" Version="3.2.0" />
|
<AssemblyAttribute Include="System.Resources.NeutralResourcesLanguageAttribute">
|
||||||
<PackageReference Include="Flurl.Serialization.TextJson" Version="3.0.0" />
|
<_Parameter1>en-US</_Parameter1>
|
||||||
<PackageReference Include="NaCl.Net" Version="0.1.13" />
|
</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>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user