Skin textures

Vanilla Minecraft servers fetch player skin texture information from Mojang servers in the form of signed base64-encoded JSON blobs. Importantly, there is no texture data included in these blobs, just URLs. Clients must fetch the actual texture data from those URLs, and the vanilla client code enforces that the URLs are subdomains of either minecraft.net or mojang.com.

The entire texture information blob is also signed by Mojang-owned private keys. Clients will reject any skin texture information that does not have a valid signature from Mojang.

The domain and signature restrictions in the vanilla client mean that the usual skin APIs are incompatible with decentralization. Skin customization is an important part of identity as well, as it's a major way for players to express themselves. By that logic, skins are well within the scope of features that Decentralized Auth should provide an alternative solution for.

Implementation details

For the sake of decentralization, it must be possible for a server to determine its own policy for skins and apply that to players who join. Players must also be able to make a direct request that the server assigns them a particular skin. The server may still choose not to honor such requests if, for example, it's a themed server that gives players a specific skin or transforms the requested skin in a certain way.

Player skin requests

As part of the login process, the server requests profile information from the player. Inside the profile request, the server includes the cached profile information for the player's last login session, if it exists. The cached profile information includes the player's previous skin hashes. This way, players don't need to send any skin textures if they haven't changed them since their last online session, saving server bandwidth.

Clients respond to the profile request by sending requested profile information, followed by any texture data that was not present in the server's cached profile information packet. Once a Decentralized Auth player logs in with a particular skin, the server should cache the skin for players who log in later, and for future logins of the same player.

Skin texture property packets

For a server to be able to apply its own skin policy to players, it must be able to send a skin texture directly to clients. The vanilla GameProfile class stores players' UUIDs and usernames, in addition to a completely freeform/extensible set of properties. Vanilla skin textures are described in the properties set, so the logical choice is for Decentralized Auth servers to use the same extensible set of properties to describe textures as well.

As per wiki.vg, the vanilla textures property is formatted as follows, encoded in base64:

{
    "timestamp": <java time in ms>,
    "profileId": "<profile uuid>",
    "profileName": "<player name>",
    "signatureRequired": true,
    "textures": {
        "SKIN": {
            "url": "<player skin URL>"
        },
        "CAPE": {
            "url": "<player cape URL>"
        }
    }
}

"timestamp", "profileId", "profileName", and "signatureRequired" are all not useful to clients since they are simply providing extra context for the integrity of the signature. Decentralized Auth clients should be modded to accept unsigned skin texture packets from the server.

The only useful data is the actual texture provided in "SKIN", "CAPE", and "ELYTRA" (although Elytra textures are embedded in the cape texture image, so "ELYTRA" seems to be unused in practice). The vanilla client supports arbitrary additional metadata in a "metadata" map alongside "url" in each of these structures. "SKIN"'s metadata may include a "model" field, with a value of either "default" or "slim".

Despite the presence of useless information in the packet, for optimal backwards and forwards compatibility, the same data structure should be preserved such that future versions can keep up with additional properties as they are added in the vanilla client. It's also best to maintain the same semantics, where the full texture data is not included directly in the packet, to prevent hogging the server bandwidth in cases where these packets are sent multiple times.

Once again, content-addressing is a good fit. Each texture can use "metadata" to store a new "textureSha256" field, holding the SHA256 hash of the image data. An additional gameplay packet will be added to support pulling a texture with a particular SHA256 hash directly from the server. Clients should cache the textures they receive to minimize bandwidth load on the server.

Custom skins in vanilla clients

Vanilla players by default would not be able to fetch skins for any Decentralized Auth players, whose UUIDs cannot have corresponding skins uploaded to official Mojang servers. It is possible to assign a player's skin using an arbitrary packet from the server-side (even if the "profileId" is for a different player), but the data still has to have been signed by Mojang. This can be accomplished with an account pool as per this Spigot forum thread. MineSkin provides an account pool like this as a public service, or server admins could optionally configure a different endpoint to use. If no signed skin data can be retrieved, there can be a fallback to a configurable default account's skin, or just default Minecraft skins.

Delivering custom skins

The vanilla server already has a packet that pushes profile information, including the skin properties above, to clients.

Decentralized Auth servers should continue to use these packets as they normally would, although the skin properties may be modified depending on the player they're being sent to and the player receiving the packet.

  • All players receive skins of vanilla players the same way as in the vanilla client (Mojang-signed packets including Mojang URL endpoints)
  • Decentralized Auth players receive skins of Decentralized Auth players directly from the server (unsigned packets with a textureSha256 that can be requested from the server)
  • Vanilla players receive skins of Decentralized Auth players from a configurable skin pool (Mojang-signed packets forwarded from the official MineSkin API by default)

Miscellaneous design choices

Why SHA256?

Although there are more modern hash algorithms available, SHA256 should be sufficient for a very long time. Mojang happens to use SHA256 content-addressing for their own skin texture endpoints; i.e. the query http://textures.minecraft.net/texture/<SHA256> will return the skin with the provided hash, and the client creates SHA256 hash-based identifiers for the in-game textures. Although there's still a lot of patching required to support decentralized skins, using the same hash algorithm keeps things slightly more unified and allows already-downloaded cached textures to work without needing to be fetched again.

SHA256 is also the default hash algorithm used by IPFS. While Decentralized Auth doesn't use IPFS by default, it'd be really neat to eventually have skin repositories that store skin textures on IPFS, and maybe even built-in IPFS nodes in the server and/or client.

Player-signed skin packets?

Theoretically, clients could generate a textures packet like the ones from Mojang, and sign them using their Decentralized Auth identity keys. This could be a neat way to ensure that the server has no way to tamper with player-generated skins, although in practice, it is very difficult for players to guarantee that the server is not MITM'ing connected users at scale. It would only be effective for users who are able to verify each others' identities through a sidechannel that is not controlled by the server admins. Thus it's probably not worth the additional complexity.