using System; using System.Buffers; using System.Buffers.Text; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using Dalamud.Bootstrap.Crypto; namespace Dalamud.Bootstrap.SqexArg { internal sealed class EncryptedArgument : IDisposable { private static char[] ChecksumTable = new char[] { 'f', 'X', '1', 'p', 'G', 't', 'd', 'S', '5', 'C', 'A', 'P', '4', '_', 'V', 'L' }; /// /// Denotes that no checksum is encoded. /// private const char NoChecksumMarker = '!'; /// /// A data that is not encrypted. /// private IMemoryOwner m_data; /// /// Creates an object that can take (e.g. /T=1234) /// /// A data that is not encrypted. /// /// This takes the ownership of the data. /// public EncryptedArgument(IMemoryOwner data) { m_data = data; } public EncryptedArgument(string argument) { var buffer = MemoryPool.Shared.Rent(Encoding.UTF8.GetByteCount(argument)); Encoding.UTF8.GetBytes(argument, buffer.Memory.Span); m_data = buffer; } public void Dispose() { m_data?.Dispose(); m_data = null!; } /// /// Extracts the payload and checksum from the encrypted argument. /// /// /// An encrypted payload encoded in url-safe base64 string extracted. The value is undefined if the function fails. /// A checksum of the key extracted. The value is undefined if the function fails. /// Returns true on success, false otherwise. public static bool Extract(string argument, out string payload, out char checksum) { // must start with //**sqex0003, some characters, one checksum character and end with **// var regex = new Regex(@"^\/\/\*\*sqex0003(?.+)(?.)\*\*\/\/$"); var match = regex.Match(argument); if (!match.Success) { payload = ""; checksum = '\0'; return false; } payload = match.Groups["payload"].Value; checksum = match.Groups["checksum"].Value[0]; return true; } public static EncryptedArgument FromEncryptedData(string argument, uint key) { // Create the key Span keyBytes = stackalloc byte[8]; CreateKey(key, keyBytes); if (!Extract(argument, out var encryptedStr, out var _)) { throw new SqexArgException($"Could not extract the argument and checksum from {argument}"); } var encryptedData = DecodeUrlSafeBase64(encryptedStr); // Allocate the buffer to store decrypted data var decryptedData = CreateAlignedBuffer(encryptedData.Length); // Decrypt the data with the key try { var blowfish = new Blowfish(keyBytes); blowfish.Decrypt(encryptedData, decryptedData.Memory.Span); } catch (Exception) { decryptedData?.Dispose(); // TODO: clean up this thing? throw; } return new EncryptedArgument(decryptedData); } /// /// /// /// Indicates that returned buffer must be larger than `minimumSize` bytes. /// /// A buffer aligned to next multiple of block size. /// Dispose() must be called when it's not used anymore. /// private static IMemoryOwner CreateAlignedBuffer(int minimumSize) { // align to next multiple of block size. var alignedSize = (minimumSize + Blowfish.BlockSize - 1) & (~(-Blowfish.BlockSize)); return MemoryPool.Shared.Rent(alignedSize); } /// /// Formats a key. /// /// A secret key. /// A buffer where formatted key will be stored. This must be larger than 8 bytes. private static void CreateKey(uint key, Span destination) { if (!Utf8Formatter.TryFormat(key, destination, out var _, new StandardFormat('x', 8))) { throw new InvalidOperationException("BUG: Could not create a key"); } } /// /// Converts the url-safe variant of base64 string to bytes. /// /// A url-safe variant of base64 string. private static byte[] DecodeUrlSafeBase64(string payload) { var base64Str = payload .Replace('-', '+') .Replace('_', '/'); return Convert.FromBase64String(base64Str); } public string Encrypt(uint key) { } } }