Skip to content

Instantly share code, notes, and snippets.

@michel-pi
Created June 13, 2021 16:50
Show Gist options
  • Save michel-pi/72a65913f7782a501248a2bb7d109de6 to your computer and use it in GitHub Desktop.
Save michel-pi/72a65913f7782a501248a2bb7d109de6 to your computer and use it in GitHub Desktop.
Generates friend codes for csgo or turns them into steam id's
using System;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
namespace Csgo
{
public static class FriendCode
{
/*
* Sources:
* https://www.unknowncheats.me/forum/counterstrike-global-offensive/453555-de-encoding-cs-friend-codes.html
* https://github.com/emily33901/go-csfriendcode
* https://github.com/emily33901/js-csfriendcode
*
*/
private const int DECODER_ALPHABET_SIZE = 'Z' + 1; // last valid character
private const string ENCODER_MASK_STRING = "CSGO\0\0\0\0";
private static readonly int[] _decoderAlphabet;
private static readonly char[] _encoderAlphabet; // lookup table
private static readonly ulong _encoderMask;
static FriendCode()
{
_encoderAlphabet = new char[]
{
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'J',
'K',
'L',
'M',
'N',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9'
};
_decoderAlphabet = new int[DECODER_ALPHABET_SIZE];
// inits lookup table
for (int i = 0; i < _encoderAlphabet.Length; i++)
{
// add 1 to i to distinguish between valid values and invalid (0). Normalize (-1) before access
_decoderAlphabet[_encoderAlphabet[i]] = i + 1;
}
// inits encoder mask
Span<byte> encoderMaskBytes = stackalloc byte[sizeof(ulong)];
Encoding.ASCII.GetBytes(ENCODER_MASK_STRING, encoderMaskBytes);
_encoderMask = BinaryPrimitives.ReadUInt64BigEndian(encoderMaskBytes);
}
private static uint GetFirstPartOfHash(ulong encodedAccountId)
{
const int DEST_SIZE = 16; // 128 bits / 8 = 16 bytes
var accountIdSpan = MemoryMarshal.CreateSpan(ref encodedAccountId, 1);
Span<byte> source = MemoryMarshal.AsBytes(accountIdSpan);
BinaryPrimitives.WriteUInt64LittleEndian(source, encodedAccountId);
Span<byte> dest = stackalloc byte[DEST_SIZE];
using var md5 = new MD5CryptoServiceProvider();
if (md5.TryComputeHash(source, dest, out var _))
{
return BinaryPrimitives.ReadUInt32LittleEndian(dest);
}
// should never happen
throw new CryptographicException();
}
private static void TransformFriendCode(ReadOnlySpan<char> source, Span<char> destination)
{
// XXXXX-YYYY
if (source.Length != 15)
{
destination[0] = 'A';
destination[1] = 'A';
destination[2] = 'A';
destination[3] = 'A';
// copies the original friend code without dashes
source.Slice(0, 5).CopyTo(destination[4..]);
source[6..].CopyTo(destination[9..]);
return;
}
// AAAA-XXXXX-YYYY
// copies the original friend code without dashes
source.Slice(0, 4).CopyTo(destination);
source.Slice(5, 5).CopyTo(destination[4..]);
source.Slice(11, 4).CopyTo(destination[9..]);
}
private static bool TryDecodeInternal(string friendCode, out ulong steamId, out Exception exception)
{
if (!TryValidateFriendCode(friendCode, out exception))
{
steamId = 0ul;
return false;
}
// AAAA-XXXXX-YYYY => AAAAXXXXXYYYY = 15 characters - the dashes
Span<char> buffer = stackalloc char[15 - 2];
TransformFriendCode(friendCode.AsSpan(), buffer);
if (!TryDecodeSpan(buffer, out var mask, out exception))
{
steamId = 0ul;
return false;
}
mask = BinaryPrimitives.ReverseEndianness(mask);
ulong accountId = 0ul;
for (int i = 0; i < 8; i++)
{
mask >>= 1;
var nibble = mask & 0xF;
mask >>= 4;
accountId <<= 4;
accountId |= nibble;
}
steamId = ToSteamId(accountId);
exception = null;
return true;
}
private static bool TryDecodeSpan(Span<char> chars, out ulong result, out Exception exception)
{
result = 0ul;
exception = null;
for (int i = 0; i < chars.Length; i++)
{
ref var current = ref chars[i];
if (current < 0 || current >= _decoderAlphabet.Length)
{
exception = new FormatException($"Friend codes can not contain the character '{current}'.");
return false; // not in lookup
}
var index = _decoderAlphabet[current] - 1; // indices are incremented by 1. normalize them
if (index < 0)
{
exception = new FormatException($"Friend codes can not contain the character '{current}'.");
return false; // not in alphabet
}
result |= (ulong)index << (i * 5); // decodes a character into 5 bits
}
return true;
}
private static bool TryEncodeInternal(ulong steamId, out string friendCode, out Exception exception)
{
var encodedAccountId = ToAccountId(steamId) | _encoderMask;
var hash = GetFirstPartOfHash(encodedAccountId);
ulong mask = 0ul;
for (int i = 0; i < 8; i++)
{
var nibble = steamId & 0xF;
steamId >>= 4;
var hashNibble = (hash >> i) & 1;
var a = mask << 4 | nibble;
// creates a ulong using hi | lo
mask = ((mask >> 28) << 32) | a;
mask = ((mask >> 31) << 32) | a << 1 | hashNibble;
}
mask = BinaryPrimitives.ReverseEndianness(mask);
return TryEncodeMask(mask, out friendCode, out exception);
}
private static bool TryEncodeMask(ulong mask, out string friendCode, out Exception ex)
{
// skips the AAAA- part from the friend code
for (int i = 0; i < 4; i++)
{
mask >>= 5;
}
Span<char> buffer = stackalloc char[10];
for (int i = 0; i < buffer.Length; i++)
{
if (i == 5)
{
buffer[i] = '-';
i++;
}
var index = unchecked((int)(mask & 0x1F));
if (index < 0 || index >= _encoderAlphabet.Length)
{
friendCode = null;
ex = new FormatException($"The index '{index}' can not be decoded into a character.");
return false;
}
buffer[i] = _encoderAlphabet[index];
mask >>= 5;
}
friendCode = new string(buffer);
ex = null;
return true;
}
private static bool TryValidateFriendCode(string friendCode, out Exception exception)
{
/*
* Possible formats:
* XXXXX-YYYY
* AAAA-XXXXX-YYYY
*/
if (friendCode == null)
{
exception = new ArgumentNullException(nameof(friendCode));
return false;
}
if (friendCode.Length == 0)
{
exception = new ArgumentException(null, nameof(friendCode));
return false;
}
if (friendCode.Length != 10 && friendCode.Length != 15)
{
exception = new FormatException("The format of a friend code has to be 'XXXXX-YYYY' or 'AAAA-XXXXX-YYYY'.");
return false;
}
if (friendCode.Length == 10 && friendCode[5] != '-')
{
exception = new FormatException("The format of a friend code has to be 'XXXXX-YYYY'.");
return false;
}
if (friendCode.Length == 15 && (friendCode[4] != '-' || friendCode[10] != '-'))
{
exception = new FormatException("The format of a friend code has to be 'AAAA-XXXXX-YYYY'.");
return false;
}
exception = null;
return true;
}
public static ulong Decode(string friendCode)
{
if (!TryDecodeInternal(friendCode, out var result, out var ex))
{
throw ex;
}
return result;
}
public static string Encode(ulong steamId)
{
if (!TryEncodeInternal(steamId, out var friendCode, out var ex))
{
throw ex;
}
return friendCode;
}
public static bool TryDecode(string friendCode, out ulong steamId)
{
return TryDecodeInternal(friendCode, out steamId, out _);
}
public static bool TryEncode(ulong steamId, out string friendCode)
{
return TryEncodeInternal(steamId, out friendCode, out _);
}
public static uint ToAccountId(ulong steamId)
{
// https://developer.valvesoftware.com/wiki/SteamID
var accountId = steamId & 0xFFFFFFFFul;
return unchecked((uint)accountId);
}
public static ulong ToSteamId(ulong accountId)
{
// https://developer.valvesoftware.com/wiki/SteamID
// consists of: Universe, Type, Instance and AccountId
return accountId | 0x110000100000000ul;
}
public static string ToSteamId2(uint accountId, bool oldValue = false)
{
// https://developer.valvesoftware.com/wiki/SteamID
var x = oldValue ? 0 : 1;
return $"STEAM_{x}:{accountId % 2}:{accountId / 2}";
}
public static string ToSteamId3(uint accountId)
{
// https://developer.valvesoftware.com/wiki/SteamID
return $"[U:1:{accountId}]";
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment