Skip to content

Commit

Permalink
Add localization, optimization, better error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
ezhevita committed Oct 3, 2024
1 parent a4778c2 commit e02654a
Show file tree
Hide file tree
Showing 19 changed files with 649 additions and 208 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
bin/
obj/
.idea
.idea
.DS_Store
*.DotSettings.user
47 changes: 36 additions & 11 deletions YandexKeyExtractor/Decryptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,28 @@

namespace YandexKeyExtractor;

public static class Decryptor
internal static class Decryptor
{
private const int maxStackallocSize = 4096;

public static string? Decrypt(string encryptedText, string password)
{
var base64Text = NormalizeBase64(encryptedText);

ReadOnlySpan<byte> textBytes = Convert.FromBase64String(base64Text).AsSpan();
var textBytes = Convert.FromBase64String(base64Text);

const byte SaltLength = 16;
var textData = textBytes[..^SaltLength];
var textSalt = textBytes[^SaltLength..];
var textData = textBytes.AsSpan()[..^SaltLength];
var salt = textBytes[^SaltLength..];

var generatedPassword = SCrypt.ComputeDerivedKey(
Encoding.UTF8.GetBytes(password), textSalt.ToArray(), 32768, 20, 1, null, 32
Encoding.UTF8.GetBytes(password),
salt,
cost: 32768,
blockSize: 20,
parallel: 1,
maxThreads: null,
derivedKeyLength: 32
);

using XSalsa20Poly1305 secureBox = new(generatedPassword);
Expand All @@ -27,8 +35,9 @@ public static class Decryptor
var nonce = textData[..NonceLength];
var dataWithMac = textData[NonceLength..];


var message = dataWithMac.Length <= 4096 ? stackalloc byte[dataWithMac.Length] : new byte[dataWithMac.Length];
var message = dataWithMac.Length <= maxStackallocSize
? stackalloc byte[dataWithMac.Length]
: new byte[dataWithMac.Length];

const byte MacLength = 16;
var data = dataWithMac[MacLength..];
Expand All @@ -41,11 +50,27 @@ public static class Decryptor

private static string NormalizeBase64(string encryptedText)
{
return encryptedText.Replace('-', '+').Replace('_', '/') + (encryptedText.Length % 4) switch
var suffixLength = (encryptedText.Length % 4) switch
{
2 => "==",
3 => "=",
_ => ""
2 => 2,
3 => 1,
_ => 0
};

var newLength = encryptedText.Length + suffixLength;
var normalized = newLength <= maxStackallocSize / sizeof(char)
? stackalloc char[newLength]
: new char[newLength];

encryptedText.CopyTo(normalized);
normalized.Replace('-', '+');
normalized.Replace('_', '/');

if (suffixLength > 0)
{
normalized[^suffixLength..].Fill('=');
}

return new string(normalized);
}
}
10 changes: 10 additions & 0 deletions YandexKeyExtractor/Exceptions/InvalidTrackIdException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;

namespace YandexKeyExtractor.Exceptions;

public class InvalidTrackIdException : Exception
{
public InvalidTrackIdException() : base("Invalid track ID.")
{
}
}
10 changes: 10 additions & 0 deletions YandexKeyExtractor/Exceptions/NoValidBackupException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;

namespace YandexKeyExtractor.Exceptions;

public class NoValidBackupException : Exception
{
public NoValidBackupException() : base(Localization.NoValidBackup)
{
}
}
22 changes: 22 additions & 0 deletions YandexKeyExtractor/Exceptions/ResponseFailedException.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 10 additions & 0 deletions YandexKeyExtractor/Models/BackupInfo.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
7 changes: 0 additions & 7 deletions YandexKeyExtractor/Models/BackupInfoResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,4 @@ public class BackupInfoResponse : StatusResponse
{
[JsonPropertyName("backup_info")]
public BackupInfo? Info { get; set; }

public class BackupInfo
{
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
[JsonPropertyName("updated")]
public uint Updated { get; set; }
}
}
3 changes: 2 additions & 1 deletion YandexKeyExtractor/Models/CountryResponse.cs
Original file line number Diff line number Diff line change
@@ -1,9 +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; }
public IReadOnlyCollection<string>? Country { get; set; }
}
9 changes: 9 additions & 0 deletions YandexKeyExtractor/Models/PhoneNumberInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;

namespace YandexKeyExtractor.Models;

public class PhoneNumberInfo
{
[JsonPropertyName("e164")]
public string? StandardizedNumber { get; set; }
}
6 changes: 0 additions & 6 deletions YandexKeyExtractor/Models/PhoneNumberResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,4 @@ public class PhoneNumberResponse : StatusResponse
{
[JsonPropertyName("number")]
public PhoneNumberInfo? PhoneNumber { get; set; }

public class PhoneNumberInfo
{
[JsonPropertyName("e164")]
public string? StandardizedNumber { get; set; }
}
}
14 changes: 14 additions & 0 deletions YandexKeyExtractor/Models/SourceGenerationContext.cs
Original file line number Diff line number Diff line change
@@ -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
{
}
3 changes: 2 additions & 1 deletion YandexKeyExtractor/Models/StatusResponse.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace YandexKeyExtractor.Models;
Expand All @@ -8,7 +9,7 @@ public class StatusResponse
public string? Status { get; set; }

[JsonPropertyName("errors")]
public string[]? Errors { get; set; }
public IReadOnlyCollection<string>? Errors { get; set; }

public bool IsSuccess => Status == "ok";
}
86 changes: 53 additions & 33 deletions YandexKeyExtractor/Program.cs
Original file line number Diff line number Diff line change
@@ -1,65 +1,85 @@
using System;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using YandexKeyExtractor;
using YandexKeyExtractor.Exceptions;

Console.WriteLine("Initializing...");
using var handler = WebHandler.Create();
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("ru-RU");
Console.WriteLine(Localization.Initializing);
using var handler = new WebHandler();

var country = await handler.TryGetCountry();

PromptInput(out var phoneNumber, "phone number");

phoneNumber = phoneNumber.TrimStart('+');
var phone = await handler.GetPhoneNumberInfo(phoneNumber, country);

var trackID = await handler.SendSMSCodeAndGetTrackID(phone, country);
if (string.IsNullOrEmpty(trackID))
string backup;
try
{
return;
}

PromptInput(out var smsCode, "SMS code");

if (!await handler.CheckCode(smsCode, trackID))
backup = await RetrieveBackup(handler);
} catch (ResponseFailedException e)
{
return;
}
if (e.Status == null)
{
Console.WriteLine(Localization.ResponseFailed, e.ResponseName);
} else
{
Console.WriteLine(Localization.ResponseFailedWithDetails, e.Status, e.ResponseName, string.Join(", ", e.Errors ?? []));
}

if (!await handler.ValidateBackupInfo(phone, trackID, country))
{
return;
}

var backup = await handler.GetBackupData(phone, trackID);
if (string.IsNullOrEmpty(backup))
} catch (NoValidBackupException)
{
Console.WriteLine(Localization.NoValidBackup);

return;
} catch (Exception e)
{
Console.WriteLine(Localization.UnknownErrorOccurred, e.Message);

throw;
}

PromptInput(out var backupPassword, "backup password");
PromptInput(out var backupPassword, Localization.BackupPasswordVariableName);

Console.WriteLine("Decrypting...");
Console.WriteLine(Localization.Decrypting);
var message = Decryptor.Decrypt(backup, backupPassword);
if (string.IsNullOrEmpty(message))
{
Console.WriteLine("Decryption failed! Most likely the password is wrong");
Console.WriteLine(Localization.DecryptionFailed);

return;
}

Console.WriteLine("Successfully decrypted!");
await File.WriteAllTextAsync("result.txt", message);
Console.WriteLine($"Written {message.Split('\n').Length} authenticators to the file (result.txt)");
Console.WriteLine(Localization.Success, message.AsSpan().Count('\n') + 1);

return;

static void PromptInput(out string result, string argumentName = "")
static async Task<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($"Enter {argumentName}:");
Console.WriteLine(Localization.PromptVariable, argumentName.ToLower(CultureInfo.CurrentCulture));
var input = Console.ReadLine();
while (string.IsNullOrEmpty(input))
{
Console.WriteLine($"{argumentName} is invalid, try again:");
Console.WriteLine(Localization.InvalidVariableValue, argumentName);
input = Console.ReadLine();
}

Expand Down
Loading

0 comments on commit e02654a

Please sign in to comment.