Created
June 13, 2021 16:50
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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