FFXIV Explained - The Login Process

posted 2022-07-28 - go back


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 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:

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:

keyvalue
lngen
rgn3
isft1 or 0
cssmode1
isnew1
launchver3

If we're on Steam, we add the following values:

keyvalue
issteam1
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:

keyvalue
_STORED_the value we obtained earlier
sqexidyour username
passwordyour password
otppwyour OTP, if any

We regex the return value of this to get the following information:

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:

keyvalue
DEV.DataPathType1
DEV.MaxEntitledExpansionIDvalue from oauth login
DEV.TestSIDsession ID
DEV.UseSqPack1
SYS.Regionvalue from oauth login
language0-3
resetConfig0
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.