FFXIV Explained - The Login Process
Explaining the FFXIV login process based on research done by XIVLauncherposted 2022-07-28
Warning
This post was made a long time ago. The information inside of it may be outdated, and the writing may not accurately reflect my current self.
For a while now, I’ve wanted to write up something interesting about FFXIV’s systems. I thought it would be a fun exercise and entertaining to write about how the login flow works, so I’m doing it!
This blog post will be aimed at semi-technical people familiar with some basic HTTP concepts, but I’ll be linking to Wikipedia pages for some technical terms, so anyone can understand the post if they wish.
If you know C#, feel free to follow along in the XIVLauncher source code.
Boot Versions
When updating the game, there are two types of updates: boot
and game
. Boot is the launcher, which requires no authentication, and game is the game files - you need to log in successfully to get the patch list. Fun fact: The files themselves aren’t authenticated - if you know the URL, you can download the patch, even without a service account.
We make an HTTP request to http://patch-bootver.ffxiv.com/http/win32/ffxivneo_release_boot/game_ver/
, where game_ver
is either the version file on disk, or the base game version (2012.01.01.0000.0000
). We also add a query string of the current time, formatted in yyyy-MM-dd-HH-mm
, where the last character is a zero. This seems to not be required - it’s probably for caching.
You can actually make this request yourself to see what it looks like:
curl http://patch-bootver.ffxiv.com/http/win32/ffxivneo_release_boot/2012.01.01.0000.0000/ -H "User-Agent: FFXIV PATCH CLIENT"
If the request returns a 204 No Content, that means we’re up to date - else, we should parse this patch list. Each line is a patch, with parameters separated by \t
. You can see the patch length (first argument), version ID, and URL to the patch. The client should then download and install these patch files, but that’s out of scope for this post.
Steam Tickets
If the user is using a Steam service account, we’ll need a ticket from the Steamworks API. Let’s break down what happens in the code here.
We initialize the Steamworks API, using the free trial app ID if chosen in the XIVLauncher settings, and get the ticket:
public static async Task<Ticket?> Get(ISteam steam)
{
var ticketBytes = await steam.GetAuthSessionTicketAsync().ConfigureAwait(true);
if (ticketBytes == null)
return null;
return EncryptAuthSessionTicket(ticketBytes, steam.GetServerRealTime());
}
Square Enix has decided to utilize their MASSIVE BRAIN once again and encrypt the ticket, for seemingly no good reason. It can’t be that bad though, right? Let’s see what’s in EncryptAuthSessionTicket
…
time -= 5;
time -= time % 60; // Time should be rounded to nearest minute.
var blowfishKey = $"{time:x08}#un@e=x>";
Ah… truly a beauty to see. Square Enix loves abusing Blowfish in literally every scenario possible. Fun fact: In FFXIV’s netcode, they use a broken implementation of Blowfish! We refer to this as “Brokefish” in the community.
Next, we do some strange things to the ticket:
- We turn it into a string, remove any dashes from the string, and turn it back to bytes. Why? Who knows!
- We create a byte array 1 byte longer than the ticket, filling the array with the ticket, and zeroing out the last byte.
- We create a sum by iterating through the ticket and adding each byte to a ushort.
- We create a BinaryWriter with a MemoryStream, write the ticket sum to it, and then write the ticket data to it.
We cast the ticket sum to a short, and use the time to create an instance of CrtRand (an implementation of the Windows libc random number generator):
int castTicketSum = unchecked((short)ticketSum);
var seed = time ^ castTicketSum;
var rand = new CrtRand((uint)seed);
We’re going to have to use Square Enix’s “fucked garbage alphabet” in this next step, to create “garbage”. If you’re curious, their fucked garbage alphabet is 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_
.
We then create garbage:
var numRandomBytes = ((ulong)(rawTicket.Length + 9) & 0xFFFFFFFFFFFFFFF8) - 2 - (ulong)rawTicket.Length;
var garbage = new byte[numRandomBytes];
uint fuckedSum = BitConverter.ToUInt32(memorySteam.ToArray(), 0);
for (var i = 0u; i < numRandomBytes; i++)
{
var randChar = FUCKED_GARBAGE_ALPHABET[(int)(fuckedSum + rand.Next()) & 0x3F];
garbage[i] = (byte)randChar;
fuckedSum += randChar;
}
We then write the garbage, and overwrite the first byte with the fuckedSum
. Now we’re going to encrypt it!
We swap the first and second bytes, and use the Blowfish key we generated earlier to create an ECB Blowfish cipher. We encrypt the bytes, and - get ready for it - turn it into a custom variant of Base64!
This Base64 is basically the same as the regular implementation, but the following characters are swapped:
+
to-
/
to_
=
to*
We then chunk the Base64 string into chunks of 300 bytes, and join these chunks with a comma. And that’s how you make a fucked up Steam ticket, Square Enix style!
Checking Gate Status
We make a request to https://frontier.ffxiv.com/worldStatus/gate_status.json
, which just returns JSON telling us if the “gates are open” - if we should continue attempting a login.
Performing an OAuth Login
We’re gonna need to use the system called OAuth (but not really like OAuth) to log in now. We’re gonna make a request to https://ffxiv-login.square-enix.com/oauth/ffxivarr/login/top
with the following query string:
key | value |
---|---|
lng | en |
rgn | 3 |
isft | 1 or 0 |
cssmode | 1 |
isnew | 1 |
launchver | 3 |
If we’re on Steam, we add the following values:
key | value |
---|---|
issteam | 1 |
session_ticket | (the ticket) |
ticket_size | (ticket length) |
We then make a request to the formed URL, and the response is: …HTML?
Yep, that’s right! There’s no API in this town, the launcher is literally just HTML being served by an Apache server. It even comes with raw, commented-with-Japanese-text JavaScript, baked into the website!
Using a regex is faster than parsing HTML, so we use it to grab a mystical value called _STORED_
. We’ll used STORED later on to exchange a session ID, which we can then use to start the game.
If we’re on Steam, we also use another regex to grab the username of the Steam account associated with this Square Enix account. We can use this to double check we’re on the right account.
We’re then gonna make a POST request to https://ffxiv-login.square-enix.com/oauth/ffxivarr/login/login.send
, with a application/x-www-form-urlencoded
form:
key | value |
---|---|
_STORED_ | the value we obtained earlier |
sqexid | your username |
password | your password |
otppw | your OTP, if any |
We regex the return value of this to get the following information:
- Your session ID
- Region of the account
- If you’ve accepted the terms of service
- If the account is “playable” (e.g. subscription paid up)
- The expansions owned by this account
Registering a session
We now have a session ID! We can use this to register a session.
Before this, we do the minimum to ensure our game isn’t corrupted by matching the .ver
and .bck
files, to make sure the version files aren’t corrupted. If the version files are corrupted, it’s a 90% chance the game is, too.
We make another POST request to https://patch-gamever.ffxiv.com/http/win32/ffxivneo_release_game/game_ver/session_id
, where game_ver
is the game version (or the fallback 2012.01.01 one), and session_id
is our session ID.
We’re going to need to send along a version report. It looks like this:
2022.03.25.0000.0001=(BOOT HASH INFO)
ex1 2022.05.26.0000.0000
ex2 2022.05.26.0000.0000
ex3 2022.05.26.0000.0000
ex4 2022.05.27.0000.0000
The hash info is an array of strings, joined by ,
. Each entry is made up of information regarding a file, to make sure none of the launcher’s files have been tampered with. Each entry looks like file_name/file_length/file_hash
.
This also will return patch info, with a slightly different structure than to boot. If we need to update the game, we do it here (again, out of scope). We also get an X-Patch-Unique-Id
header, which we get to keep for later.
If we get a 409 Conflict, that means we’re in the “everything is fucked” state. This means the files have been tampered with, most likely by a third party, and that we should panic! D: A 410 Gone means that one of the versions we have isn’t being serviced by the launcher anymore.
Starting the game
Finally! We can start ffxiv_dx11.exe
with the following arguments, in key=value
form:
key | value |
---|---|
DEV.DataPathType | 1 |
DEV.MaxEntitledExpansionID | value from oauth login |
DEV.TestSID | session ID |
DEV.UseSqPack | 1 |
SYS.Region | value from oauth login |
language | 0 -3 |
resetConfig | 0 |
ver | .ver file from game install |
I find it quite funny the “Test SID” is the legitimate session ID used to start the game. Ah, Square Enix…
We also add IsSteam=1
and set the environment variable IS_FFXIV_LAUNCH_FROM_STEAM
to 1
when launching through Steam.
The game also (optionally) accepts encrypted arguments, based on the current time. It’s a bit too much to explain, so see here if you’re interested.
Closing thoughts
Welp, that was a waste of a few hours. Maybe in a future post I’ll cover the lobby server, based on research from Sapphire…
I hope you enjoyed reading this! Sorry if I got a bit rambly at times.