using System; using System.Buffers; using System.Buffers.Text; using System.Runtime.InteropServices; using System.Text; using Dalamud.Bootstrap.Crypto; namespace Dalamud.Bootstrap.SqexArg { internal sealed class EncodedArgument : 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 EncodedArgument(IMemoryOwner data) { m_data = data; } public EncodedArgument(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!; } /// /// /// /// /// public static EncodedArgument Parse(string argument) { if (argument.Length <= 17) { // does not contain: //**sqex0003 + payload + checksum + **// var exMessage = $"The string ({argument}) is too short to parse the encoded argument." + $" It should be atleast large enough to contain the start marker, end marker, payload and checksum."; throw new SqexArgException(exMessage); } if (!argument.StartsWith("//**sqex0003") || !argument.EndsWith("**//")) { var exMessage = $"The string ({argument}) doesn't look like the valid argument." + $" It should start with //**sqeex003 and end with **// string."; throw new SqexArgException(exMessage); } // Extract the data var checksum = argument[^5]; var encryptedData = DecodeUrlSafeBase64(argument.Substring(12, argument.Length - 1 - 12 - 4)); // //**sqex0003, checksum, **// // Dedice a partial key from the checksum var (partialKey, recoverStep) = RecoverKeyFragmentFromChecksum(checksum); var decryptedData = MemoryPool.Shared.Rent(encryptedData.Length); if (!RecoverKey(encryptedData, decryptedData.Memory.Span, partialKey, recoverStep)) { // we need to free the memory to avoid a memory leak. decryptedData.Dispose(); var exMessage = $"Could not find a valid key to decrypt the encoded argument."; throw new SqexArgException(exMessage); } return new EncodedArgument(decryptedData); } private static bool RecoverKey(ReadOnlySpan encryptedData, Span decryptedData, uint partialKey, uint recoverStep) { Span keyBytes = stackalloc byte[8]; var keyCandicate = partialKey; while (true) { if (!CreateKey(keyBytes, keyCandicate)) { var message = $"BUG: Could not create a key"; // This should not fail but.. throw new InvalidOperationException(message); } var blowfish = new Blowfish(keyBytes); blowfish.Decrypt(encryptedData, decryptedData); // Check if the decrypted data looks valid if (CheckDecryptedData(decryptedData)) { return true; } // Try again with the next key. try { keyCandicate = checked(keyCandicate + recoverStep); } catch (OverflowException) { // We've exhausted the key space and could not find a valid key. return false; } } } private static bool CheckDecryptedData(ReadOnlySpan decryptedData) { // TODO return false; } private static bool CreateKey(Span destination, uint key) => Utf8Formatter.TryFormat(key, destination, out var _, new StandardFormat('X', 8)); /// /// Deduces a partial key from the checksum. /// /// /// `partialKey` can be or'd (a | partialKey) to recover some bits from the key. /// /// /// The partialKey here is very useful because it can further reduce the number of possible key /// from 0xFFFF to 0xFFF which is 16 times smaller. (and therefore we can initialize the blowfish 16 times less which is quite expensive to do so.) /// private static (uint partialKey, uint step) RecoverKeyFragmentFromChecksum(char checksum) { return MemoryExtensions.IndexOf(ChecksumTable, checksum) switch { -1 => (0x0001_0000, 0x0001_0000), // This covers '!' as well (no checksum are encoded) var index => ((uint) (index << 16), 0x0010_0000) }; } /// /// 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); } } }