implement crypto.cc with OpenSSL + fix config loading

I considered reversing the original `vlong` implementation but
OpenSSL is just simpler, more secure, and most importantly, is
already there. I also made it so the key is now stored in a PEM
file (`tibia.pem`) so it can be easily swapped without re-compiling.

  This was the last piece to build and run a working executable,
except that we now need to implement a query manager for the server
to communicate with. I was able to fix a small problem with loading
the config before hitting the problem with the query manager, which
was expected.

  The next task should be getting a query manager up and running
before starting phase two of upgrading and fixing the server itself.
This commit is contained in:
fusion32 2025-07-03 10:10:27 -03:00
parent e782274ade
commit e65dc561ac
12 changed files with 212 additions and 3586 deletions

View File

@ -3,8 +3,8 @@ BUILDDIR = build
OUTPUTEXE = game
CC = g++
CFLAGS = -m64 -fno-strict-aliasing -pedantic -Wall -Wextra -Wno-unused-parameter -Wno-format-truncation -std=c++11 -DOS_LINUX=1 -DARCH_X64=1
LFLAGS = -Wl,-t
CFLAGS = -m64 -fno-strict-aliasing -pedantic -Wall -Wextra -Wno-unused-parameter -Wno-format-truncation -std=c++11 -pthread -DOS_LINUX=1 -DARCH_X64=1
LFLAGS = -Wl,-t -lcrypto
DEBUG ?= 0
ifneq ($(DEBUG), 0)
@ -16,7 +16,7 @@ endif
HEADERS = $(SRCDIR)/common.hh $(SRCDIR)/communication.hh $(SRCDIR)/config.hh $(SRCDIR)/connections.hh $(SRCDIR)/containers.hh $(SRCDIR)/cr.hh $(SRCDIR)/crypto.hh $(SRCDIR)/enums.hh $(SRCDIR)/houses.hh $(SRCDIR)/info.hh $(SRCDIR)/magic.hh $(SRCDIR)/map.hh $(SRCDIR)/moveuse.hh $(SRCDIR)/objects.hh $(SRCDIR)/operate.hh $(SRCDIR)/query.hh $(SRCDIR)/reader.hh $(SRCDIR)/script.hh $(SRCDIR)/threads.hh $(SRCDIR)/writer.hh
$(BUILDDIR)/$(OUTPUTEXE): $(BUILDDIR)/communication.obj $(BUILDDIR)/config.obj $(BUILDDIR)/connections.obj $(BUILDDIR)/cract.obj $(BUILDDIR)/crcombat.obj $(BUILDDIR)/crmain.obj $(BUILDDIR)/crnonpl.obj $(BUILDDIR)/crplayer.obj $(BUILDDIR)/crskill.obj $(BUILDDIR)/crypto.obj $(BUILDDIR)/houses.obj $(BUILDDIR)/info.obj $(BUILDDIR)/magic.obj $(BUILDDIR)/main.obj $(BUILDDIR)/map.obj $(BUILDDIR)/moveuse.obj $(BUILDDIR)/objects.obj $(BUILDDIR)/operate.obj $(BUILDDIR)/query.obj $(BUILDDIR)/reader.obj $(BUILDDIR)/receiving.obj $(BUILDDIR)/script.obj $(BUILDDIR)/sending.obj $(BUILDDIR)/shm.obj $(BUILDDIR)/strings.obj $(BUILDDIR)/threads.obj $(BUILDDIR)/time.obj $(BUILDDIR)/utils.obj $(BUILDDIR)/writer.obj
$(CC) $(CFLAGS) $(LFLAGS) -o $@ $^
$(CC) $(CFLAGS) -o $@ $^ $(LFLAGS)
$(BUILDDIR)/communication.obj: $(SRCDIR)/communication.cc $(HEADERS)
@mkdir -p $(@D)

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +0,0 @@
struct TDatabasePoolConnection {
TDatabaseConnectionPool *DatabaseConnectionPool;
TDatabaseConnection *DatabaseConnection;
bool TransactionRunning;
};
struct TDatabaseConnectionPool {
int NumberOfConnections;
struct TDatabaseSettings DatabaseSettings;
struct TDatabaseConnection **DatabaseConnection;
bool *DatabaseConnectionFree;
struct Semaphore FreeDatabaseConnections;
struct Semaphore DatabaseConnectionMutex;
};
// TODO(fusion): Opaque struct?
struct TDatabaseConnection {};
struct TPreparedQuery {
int Number;
char *Query;
char *DBTypes;
char *CTypes;
char *Name;
};
struct TNameLockOrderData {
ulong GamemasterID;
char GamemasterName[30];
char CharacterName[30];
bool OldNameVisible;
};
struct TNotationOrderData {
ulong GamemasterID;
char GamemasterName[30];
char CharacterName[30];
char Comment[200];
};

View File

@ -23,16 +23,6 @@
#define MAX_COMMUNICATION_THREADS 1100
#define COMMUNICATION_THREAD_STACK_SIZE ((int)KB(64))
static const char RSA_PRIME_P[] =
"1201758001370723323398753778257470257713354828752713123415294815"
"0506251412291888866940292054989907714155267326586216043845592229"
"084368540020196135619327879";
static const char RSA_PRIME_Q[] =
"1189892136861686835188050824611210139447876026576932541274639840"
"5473436969889506919017477758618276066588858607419440134394668095"
"105156501566867770737187273";
static int TERMINALVERSION[3] = {770, 770, 770};
static int TCPSocket;
static ThreadHandle AcceptorThread;
@ -1533,7 +1523,9 @@ void InitCommunication(void){
AcceptorThreadPID = 0;
ActiveConnections = 0;
QueryManagerConnectionPool.init();
PrivateKey.init(RSA_PRIME_P, RSA_PRIME_Q);
// TODO(fusion): This is arbitrary, should probably be set in the config.
PrivateKey.initFromFile("tibia.pem");
OpenSocket();
if(TCPSocket == -1){

View File

@ -106,6 +106,7 @@ void ReadConfig(void){
MANAGER_DATABASE.Database[0] = 0;
char FileName[4096];
#if 0
if(const char *Home = getenv("home")){
snprintf(FileName, sizeof(FileName), "%s/.tibia", Home);
}else if(const char *Home = getenv("HOME")){
@ -113,6 +114,9 @@ void ReadConfig(void){
}else{
snprintf(FileName, sizeof(FileName), ".tibia");
}
#else
snprintf(FileName, sizeof(FileName), ".tibia");
#endif
if(!FileExists(FileName)){
throw "cannot find config-file";
@ -130,7 +134,7 @@ void ReadConfig(void){
}
char Identifier[MAX_IDENT_LENGTH];
strcpy(Identifier, Script.readIdentifier());
strcpy(Identifier, Script.getIdentifier());
Script.readSymbol('=');
// TODO(fusion): Ughh... Get rid of all `strcpy`s. A malicious configuration

View File

@ -1,3 +1,108 @@
#include "crypto.hh"
// TODO
#include <openssl/err.h>
#include <openssl/pem.h>
static void DumpOpenSSLErrors(const char *Where, const char *What){
error("OpenSSL error(s) while executing %s at %s:\n", What, Where);
ERR_print_errors_cb(
[](const char *str, usize len, void *u) -> int {
// NOTE(fusion): These error strings already have trailing newlines,
// for whatever reason.
error("> %s", str);
return 1;
}, NULL);
}
// TRSAPrivateKey
// =============================================================================
TRSAPrivateKey::TRSAPrivateKey(void){
m_RSA = NULL;
}
TRSAPrivateKey::~TRSAPrivateKey(void){
if(m_RSA){
RSA_free(m_RSA);
m_RSA = NULL;
}
}
void TRSAPrivateKey::initFromFile(const char *FileName){
if(m_RSA != NULL){
error("TRSAPrivateKey::init: Key already initialized.\n");
return;
}
FILE *File = fopen(FileName, "rb");
if(File == NULL){
error("TRSAPrivateKey::initFromFile: Failed to open \"%s\".\n", FileName);
return;
}
m_RSA = PEM_read_RSAPrivateKey(File, NULL, NULL, NULL);
fclose(File);
if(m_RSA == NULL){
error("TRSAPrivateKey::initFromFile: Failed to read key from \"%s\".\n", FileName);
DumpOpenSSLErrors("TRSAPrivateKey::initFromFile", "PEM_read_RSAPrivateKey");
}else if(RSA_size(m_RSA) != 128){
error("TRSAPrivateKey::initFromFile: File \"%s\" doesn't contain a 1024-bit key", FileName);
RSA_free(m_RSA);
m_RSA = NULL;
}
}
void TRSAPrivateKey::decrypt(uint8 *Data){
if(m_RSA == NULL){
error("TRSAPrivateKey::decrypt: Key not initialized.\n");
return;
}
// TODO(fusion): Pass in the length of `Data` for checking.
ASSERT(RSA_size(m_RSA) == 128);
if(RSA_private_decrypt(128, Data, Data, m_RSA, RSA_NO_PADDING) == -1){
DumpOpenSSLErrors("TRSAPrivateKey::decrypt", "RSA_private_decrypt");
}
}
// TXTEASymmetricKey
// =============================================================================
void TXTEASymmetricKey::init(TReadBuffer *Buffer){
m_SymmetricKey[0] = Buffer->readQuad();
m_SymmetricKey[1] = Buffer->readQuad();
m_SymmetricKey[2] = Buffer->readQuad();
m_SymmetricKey[3] = Buffer->readQuad();
}
void TXTEASymmetricKey::encrypt(uint8 *Data){
// TODO(fusion): This assumes both data endpoints have the same byte order.
// It's unlikely that there is anything other than little-endian but we
// should use a few helping functions to ensure compatibility.
uint32 Sum = 0x00000000UL;
uint32 Delta = 0x9E3779B9UL;
uint32 V0 = *(uint32*)(&Data[0]);
uint32 V1 = *(uint32*)(&Data[4]);
for(int i = 0; i < 32; i += 1){
V0 += (((V1 << 4) ^ (V1 >> 5)) + V1) ^ (Sum + m_SymmetricKey[Sum & 3]);
Sum += Delta;
V1 += (((V0 << 4) ^ (V0 >> 5)) + V0) ^ (Sum + m_SymmetricKey[(Sum >> 11) & 3]);
}
*(uint32*)(&Data[0]) = V0;
*(uint32*)(&Data[4]) = V1;
}
void TXTEASymmetricKey::decrypt(uint8 *Data){
// TODO(fusion): Same as above.
uint32 Sum = 0xC6EF3720UL;
uint32 Delta = 0x9E3779B9UL;
uint32 V0 = *(uint32*)(&Data[0]);
uint32 V1 = *(uint32*)(&Data[4]);
for(int i = 0; i < 32; i += 1){
V1 -= (((V0 << 4) ^ (V0 >> 5)) + V0) ^ (Sum + m_SymmetricKey[(Sum >> 11) & 3]);
Sum -= Delta;
V0 -= (((V1 << 4) ^ (V1 >> 5)) + V1) ^ (Sum + m_SymmetricKey[Sum & 3]);
}
*(uint32*)(&Data[0]) = V0;
*(uint32*)(&Data[4]) = V1;
}

View File

@ -2,52 +2,20 @@
#define TIBIA_CRYPTO_HH_ 1
#include "common.hh"
// TODO(fusion): We might want to scrap this implementation and use OpenSSL instead.
struct vlong_flex_unit{
uint32 n;
uint32 *a;
uint32 z;
};
struct vlong_value: vlong_flex_unit {
uint32 share;
};
struct vlong{
vlong_value *value;
int negative;
};
struct vlong_montgomery{
vlong R;
vlong R1;
vlong m;
vlong n1;
vlong T;
vlong k;
uint32 N;
};
#include <openssl/rsa.h>
struct TRSAPrivateKey{
TRSAPrivateKey(void);
~TRSAPrivateKey(void);
void init(const char *PrimeP, const char *PrimeQ);
void initFromFile(const char *FileName);
void decrypt(uint8 *Data); // single 128 bytes block
// DATA
// =================
vlong m_PrimeP;
vlong m_PrimeQ;
vlong m_U;
vlong m_DP;
vlong m_DQ;
RSA *m_RSA;
};
struct TXTEASymmetricKey{
TXTEASymmetricKey(void);
~TXTEASymmetricKey(void);
void init(TReadBuffer *Buffer);
void encrypt(uint8 *Data); // single 8 bytes block
void decrypt(uint8 *Data); // single 8 bytes block

View File

@ -216,7 +216,7 @@ void LoadWorldConfig(void){
static void InitAll(void){
try{
ReadConfig();
//SetQueryManagerLoginData(1, WorldName);
SetQueryManagerLoginData(1, WorldName);
LoadWorldConfig();
InitSHM(!BeADaemon);
LockGame();

View File

@ -187,4 +187,6 @@ struct TQueryManagerPoolConnection{
TQueryManagerConnection *QueryManagerConnection;
};
void SetQueryManagerLoginData(int Type, const char *Data);
#endif //TIBIA_QUERY_HH_

15
tibia.pem Normal file
View File

@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDLoibNahc41DeQhZUTth3FrhMNYek5xbQrlZ0O4BFr0LukdGY1
A3xHKAXt4d6Y539cQxpKqVPb+eSo44076c1PfL31BTDNFToN4uB3eUsOw3hROPSx
0Jkh72FAIhl6QynId0AHyrqeAgY59ZsTyXyJCSCWpFnl0WvK3Qf6nVBH/wIDAQAB
AoGAB9DoA19sl8JRhasS70hAuUs2sP9Ol+iWQ0wBVMZV9Nj0stnC6IsDNKn9HEXc
qOrN0SlEM5RvQxTC6ZaeX6vYNQDSJj/aigSyiNTMvw+38TD09z6EBlI4+0piGIJd
qY1ky0w3bdVhWiY2VFyGZ50NkfwjrpZSdLmsbhExzR4b/mkCQQDldLUeePo3RKZD
7+CVLFkTX/Yof8RLPOh1nukgftpvgaGcD1FXSwrIyKCHVAuwFnGgPfEr/2OAj60D
7Ya6XPeHAkEA4zC3U67ukzQxfyELrLTUBnsnKfkxejySsJ/M29SKCuHV2NOWxO8W
s9/c0RFvp0wI4uOxH00fIPYyI2h6TpxZyQJBAKQ0ZPktskKjCilMHPgkCIro/Yv2
A0+kgubJliP/I+rwZer8u0UxGsKdcOPnrYWSSjZWnaTS2y5Bo5tP/D6aETkCQDAc
AtZPtumpJcob/1LlP/jXX2W+BUIzTYTlcgYjLdA8HoK527V8Q7x3bVVAcfplWYRi
XwGX3T2npNpmp2+6IDECQBZuNbXiwHhDdsH7sSt36BGTFJNtVk3GJl/Bf50VrL6N
6NBNU3d4jQwArvSsanFYrZw+ayxS4rcJdZasO5mm+EI=
-----END RSA PRIVATE KEY-----

71
tools/genpem.go Normal file
View File

@ -0,0 +1,71 @@
package main
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"math/big"
"os"
)
const (
P = "1201758001370723323398753778257470257713354828752713123415294815" +
"0506251412291888866940292054989907714155267326586216043845592229" +
"084368540020196135619327879"
Q = "1189892136861686835188050824611210139447876026576932541274639840" +
"5473436969889506919017477758618276066588858607419440134394668095" +
"105156501566867770737187273"
E = "65537"
)
func main() {
// NOTE(fusion): Generate key from known P, Q, and E. There isn't a helper
// function from the standard library so we need to build the private key
// ourselves.
p, ok := new(big.Int).SetString(P, 10)
if !ok || !p.ProbablyPrime(4) {
panic("invalid P")
}
q, ok := new(big.Int).SetString(Q, 10)
if !ok || !q.ProbablyPrime(4) {
panic("invalid Q")
}
e, ok := new(big.Int).SetString(E, 10)
if !ok || !e.IsInt64() || !e.ProbablyPrime(4) {
panic("invalid E")
}
pMinus1 := new(big.Int).Sub(p, big.NewInt(1))
qMinus1 := new(big.Int).Sub(q, big.NewInt(1))
gcd := new(big.Int).GCD(nil, nil, pMinus1, qMinus1)
phi := new(big.Int).Mul(pMinus1, qMinus1)
lambda := new(big.Int).Div(phi, gcd)
d := new(big.Int).ModInverse(e, lambda)
n := new(big.Int).Mul(p, q)
privateKey := rsa.PrivateKey{
PublicKey: rsa.PublicKey{
N: n,
E: int(e.Int64()),
},
D: d,
Primes: []*big.Int{p, q},
}
// NOTE(fusion): `Validate()` will only perform minor sanity checks. To actually
// check that the output key is valid use `openssl rsa -in KEY.PEM -check`.
if err := privateKey.Validate(); err != nil {
panic("invalid private key: " + err.Error())
}
// NOTE(fusion): Dump private key into stdout.
block := pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(&privateKey),
}
pem.Encode(os.Stdout, &block)
}

View File

@ -21,11 +21,13 @@ var (
"-Wno-unused-parameter",
"-Wno-format-truncation",
"-std=c++11",
"-pthread",
"-DOS_LINUX=1",
"-DARCH_X64=1",
}
linkerOptions = []string{
"-Wl,-t",
"-lcrypto",
}
)
@ -93,7 +95,7 @@ func main() {
fmt.Fprintf(&output, " $(BUILDDIR)/%v", object.obj)
}
fmt.Fprint(&output, "\n")
fmt.Fprint(&output, "\t$(CC) $(CFLAGS) $(LFLAGS) -o $@ $^\n\n")
fmt.Fprint(&output, "\t$(CC) $(CFLAGS) -o $@ $^ $(LFLAGS)\n\n")
// OBJECTS
for _, object := range objectFiles {