done with printing stuff, saving is non trivial due to classes without constructors or something

This commit is contained in:
Rusty Striker 2025-01-06 20:27:54 +02:00
parent 2aa4a86e5f
commit 2a3be450be
Signed by: RustyStriker
GPG key ID: 87E4D691632DFF15
8 changed files with 277 additions and 109 deletions

View file

@ -1,6 +1,9 @@
# Project - TODO:
[ ] Do print stuff for EVERYTHING (with colors preferably)
[x] Client
[x] Server
[ ] rewrite the protocol.md
[ ] Make a video that shows how everything works yada yada
[ ] export everything and upload it
[ ] get a nice drink to celebrate

44
client/Log.cs Normal file
View file

@ -0,0 +1,44 @@
using System;
using lib;
namespace client;
/// Helper class to pretty print stuff with colors, client specific
public static class Log
{
public static void Encrypted(string message, byte[] payload)
{
Console.ForegroundColor = Utils.C_ENC;
Console.WriteLine($"{message}: {Convert.ToBase64String(payload)}");
Console.ResetColor();
}
public static void Decrypted(string message, byte[] payload)
{
Console.ForegroundColor = Utils.C_DEC;
Console.WriteLine($"{message}: {Convert.ToBase64String(payload)}");
Console.ResetColor();
}
public static void Decrypted(string message)
{
Console.ForegroundColor = Utils.C_DEC;
Console.WriteLine(message);
Console.ResetColor();
}
public static void System(string message)
{
Console.ForegroundColor = Utils.C_SYS;
Console.WriteLine(message);
Console.ResetColor();
}
public static void Message(string message)
{
Console.WriteLine(message);
}
}

View file

@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Serialization;
using lib;
namespace client;
@ -14,6 +15,7 @@ public class Message
public string Sender { set; get; }
public bool IsAck { set; get; }
public string Content { set; get; }
[JsonIgnore]
public byte[] Signature { set; get; }
public Message(byte[] bytes)

View file

@ -8,6 +8,7 @@ using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using client;
using lib;
@ -18,10 +19,11 @@ public class Program
{
static string User = "";
static readonly Random Rand = new((int)DateTime.Now.Ticks);
static byte counter = (byte)Rand.Next();
static byte Counter = (byte)Rand.Next();
static async Task Main(string[] args)
{
// Load some info stuff
User = args
.SkipWhile(a => !new[] { "-p", "--phone" }.Contains(a)) // search for the option
.Skip(1) // skip the `-u/--user` itself to get the value
@ -29,22 +31,32 @@ public class Program
// On boot, check if a key is available
RSA? serverKey = LoadRSAFromFile("server_key.pem");
if (serverKey == null) { Console.WriteLine("Could not find server key, please run server before clients!"); return; }
RSA pubKey = LoadRSAFromFile($"pubkey_{User}.pem") ?? RSA.Create(2048);
if (serverKey == null)
{
Log.System("Could not find server key, please run server before clients!");
return;
}
// Attempt to load the user's keys,
RSA pubKey = LoadRSAFromFile($"pubkey_{User}.pem") ?? RSA.Create(1024);
RSA privKey = LoadRSAFromFile($"privkey_{User}.pem") ?? pubKey;
SaveRSAKeys(User, pubKey, privKey);
using TcpClient client = new("127.0.0.1", 12345);
// if pubKey and privKey is the same object then we created the keys just now, otherwise we can use the below flags to force a registration
bool needsRegister = pubKey == privKey || args.Any(a => new[] { "-fr", "--force-register" }.Contains(a));
// Connect to the server
using TcpClient client = new("127.0.0.1", 12345);
var stream = client.GetStream();
// First contact init
bool needsRegister = pubKey == privKey || args.Any(a => new[] { "-fr", "--force-register" }.Contains(a));
// Establish secure connection
Aes sk = Aes.Create(); // creates an AES-256 key
// establish secure connection
Log.Decrypted($"Session key + IV", [.. sk.Key, .. sk.IV]);
byte[] skEnc = serverKey.Encrypt([.. sk.Key, .. sk.IV], RSAEncryptionPadding.OaepSHA256);
Log.Encrypted("Session key + IV", skEnc);
await stream.WriteAsync(skEnc);
// wait for the server to confirm it recieved the keys
// wait for the server to confirm it recieved the keys, supposedly we would want to read exactly what we need then
// send as much as we can in 1 go (and maybe use buffered streams), but i didnt do that and im fine with that
await stream.ReadExactlyAsync(new byte[1]);
if (needsRegister)
@ -55,38 +67,48 @@ public class Program
}
catch (Exception ex)
{
Console.WriteLine("Failed registration process");
Console.WriteLine("Exception: " + ex.Message);
Console.WriteLine("Stack: " + ex.StackTrace);
Log.System("Failed registration process");
Log.System("Exception: " + ex.Message);
Log.System("Stack: " + ex.StackTrace);
return;
}
}
else
{
// start login process
byte[] msg = Request.CreateRequest(RequestType.Login, ref counter, Utils.NumberToBytes(User));
byte[] msg = Request.CreateRequest(RequestType.Login, ref Counter, Utils.NumberToBytes(User));
Log.Decrypted("Login", msg);
msg = sk.EncryptCfb(msg, sk.IV, PaddingMode.PKCS7);
Log.Encrypted("Login", msg);
await stream.WriteAsync(msg);
byte[] toSign = new byte[16];
await stream.ReadExactlyAsync(toSign, 0, 16);
Log.Encrypted("Challenge", toSign);
toSign = sk.DecryptCfb(toSign, sk.IV, PaddingMode.None);
Log.Decrypted("Challenge", toSign);
byte[] signed = privKey.SignData(toSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
msg = Request.CreateRequest(RequestType.ConfirmLogin, ref counter, BitConverter.GetBytes(signed.Length));
Log.Decrypted("Signed", signed);
msg = Request.CreateRequest(RequestType.ConfirmLogin, ref Counter, BitConverter.GetBytes(signed.Length));
Log.Decrypted("Challenge message", msg);
msg = sk.EncryptCfb(msg, sk.IV, PaddingMode.None);
WriteColor($"Sending sig: {Convert.ToBase64String(signed)}", ConsoleColor.Green);
Log.Encrypted("Challenge message", msg);
msg = [.. msg, .. signed];
Log.Encrypted("Challenge final", msg);
await stream.WriteAsync(msg);
byte[] buffer = new byte[512];
int len = await stream.ReadAsync(buffer);
Log.Encrypted("Result", buffer[..len]);
byte[] dec = sk.DecryptCfb(buffer[..len], sk.IV, PaddingMode.PKCS7);
Log.Decrypted("Result", dec);
string r = Encoding.UTF8.GetString(dec);
Log.Decrypted($"Result (UTF8): {JsonSerializer.Serialize(r.ToCharArray())}");
if (r == "OK")
{
Console.WriteLine("Login successful");
Log.System("Login successful");
}
else
{
Console.WriteLine($"Failed login: {r}");
Log.System($"Failed login: {r}");
client.Dispose();
return;
}
@ -98,34 +120,29 @@ public class Program
async static Task RegisterClient(string user, RSA pub, RSA priv, Aes sk, NetworkStream stream)
{
Console.WriteLine("Attempting to register with public key:");
Console.WriteLine(pub.ExportRSAPublicKeyPem());
// Generate aes key and send it forward
Console.WriteLine($"Session key: {string.Join(' ', sk.Key)}");
Console.WriteLine($"Session IV: {string.Join(' ', sk.IV)}");
Log.System("Attempting to register with public key:");
Log.System(pub.ExportRSAPublicKeyPem());
// Generate the Register msg
Console.WriteLine("Sending rsa public key thing");
byte[] pubBytes = pub.ExportRSAPublicKey();
byte[] data = new byte[12];
Array.Copy(Utils.NumberToBytes(user), data, 8);
Array.Copy(BitConverter.GetBytes(pubBytes.Length), 0, data, 8, 4);
byte[] msg = Request.CreateRequest(RequestType.Register, ref counter, data);
byte[] msg = Request.CreateRequest(RequestType.Register, ref Counter, data);
// Encrypt msg and send it
byte[] payload = [.. msg, .. pubBytes];
Log.Decrypted("Register payload", payload);
byte[] enc = sk.EncryptCfb(payload, sk.IV, PaddingMode.PKCS7);
Console.WriteLine($"payload length: {enc.Length}");
Log.Encrypted("Register payload", enc);
await stream.WriteAsync(enc);
// get the 6 digit code (from "secure channel", actually an OK message is expected here but the 6 digit code kinda replaces it)
byte[] digits = new byte[6];
int len = 0;
while (len != 6)
{
len = await stream.ReadAsync(digits);
}
int len = await stream.ReadAsync(digits);
Log.System($"6 digit code actual length: {len}");
// print the 6 digit code
Console.WriteLine($"[{DateTime.Now}] 6 digit code: {string.Join(' ', digits.Select(d => d.ToString()))}");
Log.System($"[{DateTime.Now}] 6 digit code: {string.Join(' ', digits.Select(d => d.ToString()))}");
// get the 6 digit code from the user
while (true)
{
@ -133,7 +150,8 @@ public class Program
string? code = Console.ReadLine()?.Trim();
if (code == null || code.Take(6).Any(c => !char.IsDigit(c)))
{
Console.WriteLine("Invalid code!");
// we know the code is invalid because it HAS to be a 6 digit code
Log.System("Invalid code!");
continue;
}
byte[] codeBytes = code
@ -141,18 +159,25 @@ public class Program
.Select(d => byte.Parse(d.ToString())) // parse into bytes
.ToArray();
// Debug print the inserted value to see it works :)
Console.WriteLine(string.Join(' ', codeBytes.Select(b => b.ToString())));
Log.System(string.Join(' ', codeBytes.Select(b => b.ToString())));
// Sign the 6 digit code & Generate ConfirmRegister message
byte[] signed = priv.SignData(codeBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
msg = Request.CreateRequest(RequestType.ConfirmRegister, ref counter, [.. codeBytes, .. BitConverter.GetBytes(signed.Length)]);
Log.Decrypted("Signed code", signed);
msg = Request.CreateRequest(RequestType.ConfirmRegister, ref Counter, [.. codeBytes, .. BitConverter.GetBytes(signed.Length)]);
Log.Decrypted("Signed message", msg);
enc = sk.EncryptCfb(msg, sk.IV, PaddingMode.None); // no reason to encrpy the signature
Log.Encrypted("Signed message", enc);
payload = [.. enc, .. signed]; // should be 128 (enc) + 256 (signed)
Log.Encrypted("Signed final", payload);
await stream.WriteAsync(payload);
// wait for OK/NACK response (anything other than OK is a NACK)
int incoming = await stream.ReadAsync(enc);
Log.Encrypted("Response", enc[..incoming]);
msg = sk.DecryptCfb(enc[..incoming], sk.IV, PaddingMode.PKCS7);
Log.Decrypted("Response", msg);
string r = Encoding.UTF8.GetString(msg);
Log.Decrypted($"Response (UTF8): {r}");
if (r == "OK")
{
Console.WriteLine("Registration process complete");
@ -189,7 +214,7 @@ public class Program
Dictionary<string, RSA> publicKeys = [];
while (client.Connected)
{
Console.Write("> ");
Console.Write((currentChat != null ? $"[{currentChat}] " : "") + "> ");
string? input = Console.ReadLine();
if (input == null)
{
@ -212,7 +237,7 @@ public class Program
currentChat = words.Length > 1 ? words[1] : null;
if (currentChat != null && currentChat.Length > 16)
{
Console.WriteLine("Invalid number: too long");
Log.System("Invalid number: too long");
currentChat = old;
continue;
}
@ -228,7 +253,7 @@ public class Program
else
{
currentChat = old;
Console.WriteLine($"Reverting to previous chat: {currentChat ?? "none"}");
Log.System($"Reverting to previous chat: {currentChat ?? "none"}");
}
}
break;
@ -244,25 +269,30 @@ public class Program
{
if (currentChat == null)
{
Console.WriteLine("No chat is active, please select chat using '/msg [number]' or '/chat [number]'");
Log.System("No chat is active, please select chat using '/msg [number]' or '/chat [number]'");
}
else if (publicKeys.TryGetValue(currentChat!, out RSA? key))
{
// Pick the id as a random uint, good enough for the sake of testing
Message m = new((uint)Rand.Next(), User, false, input);
m.CalculateSignature(privKey);
Log.Decrypted($"Message: {JsonSerializer.Serialize(m)}");
Log.Decrypted("Message sig", m.Signature);
byte[] userMsg = [.. key.Encrypt(m.Bytes(), RSAEncryptionPadding.OaepSHA256), .. m.Signature];
Log.Encrypted("Message", userMsg);
byte[] req = Request.CreateRequest(
RequestType.SendMessage,
ref counter,
ref Counter,
[.. Utils.NumberToBytes(currentChat!), .. BitConverter.GetBytes(userMsg.Length)]);
Log.Decrypted("Message req", req);
req = sk.EncryptCfb(req, sk.IV);
Log.Encrypted("Message req", req);
stream.Write([.. req, .. userMsg]);
Console.WriteLine($"[{DateTime.Now}] Sent to server: {input}");
Log.System($"[{DateTime.Now}] Sent to server: {input}");
}
else
{
Console.WriteLine($"active chat exists, but no key was found...");
Log.System($"active chat exists, but no key was found...");
}
}
}
@ -270,45 +300,50 @@ public class Program
static async Task<RSA?> GetPublicKey(NetworkStream stream, Aes sk, string chat)
{
byte[] req = Request.CreateRequest(RequestType.GetUserKey, ref counter, Utils.NumberToBytes(chat));
byte[] req = Request.CreateRequest(RequestType.GetUserKey, ref Counter, Utils.NumberToBytes(chat));
Log.Decrypted("Get key req", req);
req = sk.EncryptCfb(req, sk.IV, PaddingMode.None); // no need for padding this is exactly 128 bytes
Log.Encrypted("Get key req", req);
await stream.WriteAsync(req);
byte[] response = new byte[1024];
int len = await stream.ReadAsync(response);
Log.Encrypted("Key", response[..len]);
byte[] key = sk.DecryptCfb(response[..len], sk.IV, PaddingMode.PKCS7);
Log.Decrypted("Key", key);
if (key[0] == 1)
{
Console.WriteLine($"failed getting key for {chat}: {Encoding.UTF8.GetString(key[1..])}");
Log.System($"failed getting key for {chat}: {Encoding.UTF8.GetString(key[1..])}");
return null;
}
else
{
RSA bobsKey = RSA.Create();
bobsKey.ImportRSAPublicKey(key.AsSpan()[1..], out int _);
Console.WriteLine($"Got key:\n {bobsKey.ExportRSAPublicKeyPem()}\n");
Log.System($"Got key:\n {bobsKey.ExportRSAPublicKeyPem()}\n");
return bobsKey;
}
}
static async Task GetMessages(NetworkStream stream, Aes sk, RSA privKey, Dictionary<string, RSA> publicKeys)
{
byte[] req = Request.CreateRequest(RequestType.GetMessages, ref counter, []);
byte[] req = Request.CreateRequest(RequestType.GetMessages, ref Counter, []);
Log.Decrypted("Get messages", req);
req = sk.EncryptCfb(req, sk.IV, PaddingMode.None); // no need for padding this is exactly 128 bytes
Log.Encrypted("Get messages", req);
await stream.WriteAsync(req);
byte[] buffer = new byte[4096];
int len = await stream.ReadAsync(buffer);
Log.Encrypted("Response", buffer[..len]);
byte[] lengths = buffer[..16];
lengths = sk.DecryptCfb(lengths, sk.IV, PaddingMode.None);
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"Got messages: {string.Join(", ", lengths)}");
Console.ResetColor();
Log.Decrypted($"Lengths: {string.Join(", ", lengths)}");
byte[] msg = new byte[1024]; // msg buffer
int start = 16; // skip the first 16 bytes since its the lengths message
for (int i = 0; i < lengths.Length - 1; i += 2)
{
int l = (lengths[i] << 8) | (lengths[i + 1]);
Console.WriteLine($"got message of length: {l}");
if (l == 0) { break; } // a 0 means we are done actually, as empty messages shouldn't be allowed
// get the msg
int end = start + l;
@ -317,17 +352,19 @@ public class Program
// TODO: properly handle it as a buffered stream and properly buffer it
}
WriteColor($"got ecnryped message: {Convert.ToBase64String(buffer[start..end])}", ConsoleColor.Green);
Log.Encrypted($"Message {i / 2}", buffer[start..end]);
// decrypt the message
int msgLen = privKey.KeySize / 8;
int msgLen = privKey.KeySize / 8; // message length is derived from the size of the key
if (privKey.TryDecrypt(buffer.AsSpan()[start..(msgLen + start)], msg, RSAEncryptionPadding.OaepSHA256, out int written))
{
byte[] dec = msg[..written];
Log.Decrypted($"Message {i / 2}", dec);
Message m = new(dec)
{
Signature = buffer[(start + msgLen)..end]
};
Log.Decrypted($"Message {i / 2}: {JsonSerializer.Serialize(m)}");
Log.Decrypted($"Message {i / 2} sig", m.Signature);
bool sigValid = false;
if (publicKeys.TryGetValue(m.Sender, out RSA? pk) || (pk = await GetPublicKey(stream, sk, m.Sender)) != null)
{
@ -336,21 +373,25 @@ public class Program
}
else
{
Console.WriteLine("Failed getting sender's public key");
Log.System("Failed getting sender's public key - cannot verify signature");
}
WriteColor($"decrypted message: {Convert.ToBase64String(dec)}", ConsoleColor.Green);
Console.WriteLine($"Got message from {m.Sender} (id: {m.Id}) (valid: {sigValid}): " + (m.IsAck ? "ACK" : m.Content));
Log.Message($"Got message from {m.Sender} (id: {m.Id}) (valid: {sigValid}): " + (m.IsAck ? "ACK" : m.Content));
// if the signature is valid we also want to send back a message ack for the same id :)
if (sigValid)
if (sigValid && !m.IsAck)
{
Message mAck = new(m.Id, User, true, "");
Log.Decrypted($"Ack {i / 2}: {JsonSerializer.Serialize(mAck)}");
mAck.CalculateSignature(privKey);
Log.Decrypted($"Ack {i / 2} sig", mAck.Signature);
byte[] userMsg = [.. publicKeys[m.Sender].Encrypt(mAck.Bytes(), RSAEncryptionPadding.OaepSHA256), .. mAck.Signature];
Log.Encrypted($"Ack {i / 2}", userMsg);
byte[] reqAck = Request.CreateRequest(
RequestType.SendMessage,
ref counter,
ref Counter,
[.. Utils.NumberToBytes(m.Sender), .. BitConverter.GetBytes(userMsg.Length)]);
Log.Decrypted($"Ack {i / 2} request", reqAck);
reqAck = sk.EncryptCfb(reqAck, sk.IV);
Log.Encrypted($"Ack {i / 2} request", reqAck);
stream.Write([.. reqAck, .. userMsg]);
}
}
@ -361,21 +402,13 @@ public class Program
// and i dont want the server to be aware of that, i think it makes more sense for the server to act as a relay
// and a buffer than an actual participant, so if the message is failing to decrypt that will go unnoticed.
// supposedly the sender will notice the lack of response and send it again
Console.WriteLine("Incoming message failed to decrypt, unknown sender");
Log.System("Incoming message failed to decrypt, unknown sender");
}
start = end;
}
bool hasMoreMessages = lengths[15] != 0;
Console.WriteLine($"hasMoreMessages: {hasMoreMessages}");
}
static void WriteColor(string Line, ConsoleColor Color, ConsoleColor Background = ConsoleColor.Black)
{
Console.ForegroundColor = Color;
Console.BackgroundColor = Background;
Console.WriteLine(Line);
Console.ResetColor();
Log.System($"hasMoreMessages: {hasMoreMessages}");
}
}

View file

@ -1,10 +1,17 @@
using System.Linq;
using System.Text;
namespace lib;
public static class Utils
{
// some print color coding constants for ease of change later
/// Encrypted stuff
public const ConsoleColor C_ENC = ConsoleColor.Green;
/// Non encrypted stuff (either before encryption or after decryption)
public const ConsoleColor C_DEC = ConsoleColor.Red;
/// System messages
public const ConsoleColor C_SYS = ConsoleColor.Yellow;
public static byte[] NumberToBytes(string Number)
{
if (Number.Any(c => !char.IsDigit(c)) || Number.Length > 16)

47
server/Log.cs Normal file
View file

@ -0,0 +1,47 @@
using System;
using lib;
namespace server;
/// Printing and helper class for logging stuff on the server, this is not static so we dont have to keep on adding the id and client at every call
public class Log(int id, string? client)
{
public int Id { set; get; } = id;
public string? Client { set; get; } = client;
public void Encrypted(string message, byte[] payload)
{
Console.ForegroundColor = Utils.C_ENC;
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {Id} [{Client ?? "unknown"}] - {message}: {Convert.ToBase64String(payload)}");
Console.ResetColor();
}
public void Decrypted(string message, byte[] payload)
{
Console.ForegroundColor = Utils.C_DEC;
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {Id} [{Client ?? "unknown"}] - {message}: {Convert.ToBase64String(payload)}");
Console.ResetColor();
}
public void Decrypted(string message)
{
Console.ForegroundColor = Utils.C_DEC;
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {Id} [{Client ?? "unknown"}] - {message}");
Console.ResetColor();
}
public void System(string message)
{
Console.ForegroundColor = Utils.C_SYS;
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {Id} [{Client ?? "unknown"}] - {message}");
Console.ResetColor();
}
public void Message(string message)
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {Id} [{Client ?? "unknown"}] - {message}");
}
}

View file

@ -7,6 +7,7 @@ using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using lib;
@ -23,7 +24,7 @@ public class Program
static async Task Main()
{
// Generally this key would be static but since its not production yet we can generate it every time to make sure
// Generally this key would be static but since its not production we can generate it every time to make sure
// the users has the key and could load it from file
RSA key = RSA.Create(1024);
File.WriteAllText("server_key.pem", key.ExportRSAPublicKeyPem());
@ -31,6 +32,14 @@ public class Program
int port = 12345;
TcpListener server = new(IPAddress.Parse("0.0.0.0"), port);
int connectionCounter = 0;
Log log = new(-1, null);
Console.CancelKeyPress += delegate
{
Console.WriteLine("\nEXITING!");
server.Stop();
};
try
{
server.Start();
@ -48,8 +57,8 @@ public class Program
}
catch (Exception ex)
{
Console.WriteLine($"Client crashed: {ex.Message}");
Console.WriteLine(ex.StackTrace);
log.System($"Client crashed: {ex.Message}");
log.System(ex.StackTrace ?? "MISSING STACK TRACE");
}
});
connectionCounter += 1;
@ -57,8 +66,8 @@ public class Program
}
catch (Exception ex)
{
Console.WriteLine($"Server error: {ex.Message}");
Console.WriteLine("Trace: " + ex.StackTrace);
log.System($"Server error: {ex.Message}");
log.System("Trace: " + ex.StackTrace);
}
finally
{
@ -68,31 +77,32 @@ public class Program
static async Task HandleClient(TcpClient client, int id, RSA pubKey)
{
Write(id, "Got a new client");
Log log = new(id, null);
log.System("Got a new client");
string clientPhone = "";
NetworkStream stream = client.GetStream();
byte[] buffer = new byte[1024];
byte counter = 0;
// Get AES session key
int len = await stream.ReadAsync(buffer);
Write(id, $"Got {len} bytes");
log.Encrypted("Key + IV", buffer[..len]);
byte[] skBytes = pubKey.Decrypt(buffer[..len], RSAEncryptionPadding.OaepSHA256);
log.Decrypted("Key + IV", skBytes);
Aes sk = Aes.Create();
sk.Key = skBytes[..32]; // just to make sure no one sends a too big to be true key
sk.IV = skBytes[32..];
Write(id, $"key: {string.Join(' ', sk.Key)}");
Write(id, $"IV: {string.Join(' ', sk.IV)}");
await stream.WriteAsync(new byte[] { 0 });
// Get first message (should be either login or register)
len = await stream.ReadAsync(buffer);
Write(id, $"Got {len} bytes");
log.Encrypted("Auth message", buffer[..len]);
byte[] msgDec = sk.DecryptCfb(buffer[..len], sk.IV, PaddingMode.PKCS7);
log.Decrypted("Auth message", msgDec);
byte[] msg = msgDec[..MSG_LEN];
Write(id, Request.RequestToString(msg));
log.Message(Request.RequestToString(msg));
if (msg[0] != 0)
{
Write(id, "Invalid session id!");
log.System("Invalid message version!");
client.Dispose();
return;
}
@ -102,13 +112,13 @@ public class Program
// Do register stuff
// get phone number
string phone = Utils.BytesToNumber(msg[3..11]);
Write(id, $"Client wants to register as {phone}");
log.System($"Client wants to register as {phone}");
clientPhone = phone;
log.Client = phone;
int keyLen = BitConverter.ToInt32(msg, 11);
RSA pub = RSA.Create();
pub.ImportRSAPublicKey(msgDec.AsSpan()[MSG_LEN..], out int bytesRead);
Write(id, $"Imported key len: {bytesRead} while client claims it is {keyLen}");
Write(id, $"Imported key is: \n {pub.ExportRSAPublicKeyPem()}\n");
log.System($"Imported key is: \n {pub.ExportRSAPublicKeyPem()}\n");
// generate the 6 digit code and send it
byte[] code = [
(byte)Rand.Next(10),
@ -118,17 +128,19 @@ public class Program
(byte)Rand.Next(10),
(byte)Rand.Next(10),
];
await Send6DigitCodeInSecureChannel(stream, code);
await SendBySecureChannel(stream, code);
// wait for the code to be back with a key
int tries = 5; // allow 5 tries before closing the connection and forcing a restart
while (tries > 0)
{
tries -= 1;
len = await stream.ReadAsync(buffer);
Write(id, $"Got 6 digit code with sig, len: {len}");
log.Encrypted("ConfirmRegister", buffer[..len]);
msg = sk.DecryptCfb(buffer[..MSG_LEN], sk.IV, PaddingMode.None);
Write(id, Request.RequestToString(msg));
log.Decrypted("ConfirmRegister", msg);
log.Message(Request.RequestToString(msg));
byte[] sig = buffer[MSG_LEN..len];
log.Decrypted("ConfirmRegister (sig)", sig);
if (msg[0] != 0 || msg[1] != (byte)RequestType.ConfirmRegister || msg[2] != counter)
{
// invalid or unexpected req, someone might be sending dups
@ -139,13 +151,16 @@ public class Program
int expectedSigLen = BitConverter.ToInt32(msg, 9);
if (expectedSigLen != len - MSG_LEN)
{
Write(id, $"expected sig len doesnt match read len: {expectedSigLen} / {len - MSG_LEN}");
log.System($"expected sig len doesnt match read len: {expectedSigLen} / {len - MSG_LEN}");
}
// check if the codes are equal
if (code.Zip(gottenCode).Any(a => a.First != a.Second))
{
// codes are not equal, send a nack
// perhaps we should only send a SIG INVALID (or just FAILED) and hide the reason in case someone tries to guess the code
msg = sk.EncryptCfb(Encoding.UTF8.GetBytes("BAD CODE"), sk.IV, PaddingMode.PKCS7);
log.Decrypted("Fail Register", Encoding.UTF8.GetBytes("BAD CODE"));
log.Encrypted("Fail Register", msg);
await stream.WriteAsync(msg);
}
else
@ -155,6 +170,8 @@ public class Program
if (sigValid)
{
msg = sk.EncryptCfb(Encoding.UTF8.GetBytes("OK"), sk.IV, PaddingMode.PKCS7);
log.Decrypted("Register Success", Encoding.UTF8.GetBytes("OK"));
log.Encrypted("Register Success", msg);
await stream.WriteAsync(msg);
Data.Keys[phone] = pub; // save the key
break;
@ -162,6 +179,8 @@ public class Program
else
{
msg = sk.EncryptCfb(Encoding.UTF8.GetBytes("SIG INVALID"), sk.IV, PaddingMode.PKCS7);
log.Decrypted("Fail Register", Encoding.UTF8.GetBytes("SIG INVALID"));
log.Encrypted("Fail Register", msg);
await stream.WriteAsync(msg);
}
}
@ -172,42 +191,51 @@ public class Program
{
// verify login
clientPhone = Utils.BytesToNumber(msg[3..11]);
log.Client = clientPhone;
counter = IncrementCounter(msg[2]);
if (!Data.Keys.TryGetValue(clientPhone, out RSA? clientKey))
{
stream.Close();
client.Close();
Write(id, $"Client claims to be {clientPhone}, but could not find key in records");
log.System($"Client claims to be {clientPhone}, but could not find key in records");
return;
}
byte[] challenge = new byte[16];
Rand.NextBytes(challenge);
Write(id, $"Sending challenge: {Convert.ToBase64String(challenge)}");
log.Decrypted("Challenge", challenge);
byte[] response = sk.EncryptCfb(challenge, sk.IV, PaddingMode.None);
log.Encrypted("Challenge", response);
await stream.WriteAsync(response);
len = await stream.ReadAsync(buffer);
log.Encrypted("Challenge Response", buffer[..len]);
msg = sk.DecryptCfb(buffer[..MSG_LEN], sk.IV, PaddingMode.None);
Write(id, Request.RequestToString(msg));
log.Decrypted("Challenge Message", msg);
log.Message(Request.RequestToString(msg));
if (msg[2] != counter)
{
client.Close();
Write(id, $"Invalid counter in login response, quitting");
log.System($"Invalid counter in login response, quitting");
return;
}
counter = IncrementCounter(counter);
byte[] sig = buffer[MSG_LEN..len];
Write(id, $"Got challenge signature, length: {len - MSG_LEN}, client says: {BitConverter.ToInt32(msg, 3)}");
Write(id, $"Sig: {Convert.ToBase64String(sig)}");
// sig wasnt encrypted so this print is somewhat redundant, but i think its still useful to see the sig alone
log.Decrypted("Challenge Sig", sig);
bool valid = clientKey.VerifyData(challenge, sig, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
if (valid)
{
log.System("Client verification complete");
response = sk.EncryptCfb(Encoding.UTF8.GetBytes("OK"), sk.IV, PaddingMode.PKCS7);
log.Decrypted("Challenge Response", Encoding.UTF8.GetBytes("OK"));
log.Encrypted("Challenge Response", response);
await stream.WriteAsync(response);
}
else
{
Write(id, "Client failed verification, invalid signature");
log.System("Client failed verification, invalid signature");
response = sk.EncryptCfb(Encoding.UTF8.GetBytes("INVALID SIG"), sk.IV, PaddingMode.PKCS7);
log.Decrypted("Challenge Response", Encoding.UTF8.GetBytes("INVALID SIG"));
log.Encrypted("Challenge Response", response);
await stream.WriteAsync(response);
client.Close();
return;
@ -216,7 +244,7 @@ public class Program
else
{
// invalid connection, quit
Write(id, "Client didnt register or login as first message");
log.System("Client didnt register or login as first message");
client.Dispose();
return;
}
@ -228,12 +256,14 @@ public class Program
// while the client is connected, simply read messages from the client and handle accordingly,
// either by getting new messages for other ppl, or sending back keys/pending messages
len = await stream.ReadAsync(buffer);
log.Encrypted("Request", buffer[..len]);
msg = sk.DecryptCfb(buffer[..MSG_LEN], sk.IV, PaddingMode.None);
Write(id, Request.RequestToString(msg));
log.Decrypted("Request message", msg);
log.Message(Request.RequestToString(msg));
// verify that the counter message is correct
if (msg[0] != 0 || msg[2] != counter)
{
msg = sk.EncryptCfb(Encoding.UTF8.GetBytes("DUPLICATE"), sk.IV, PaddingMode.PKCS7);
msg = sk.EncryptCfb(Encoding.UTF8.GetBytes("INVALID REQUEST"), sk.IV, PaddingMode.PKCS7);
await stream.WriteAsync(msg);
continue;
}
@ -244,7 +274,6 @@ public class Program
byte[] msgsLens = Enumerable.Repeat<byte>(0, 16).ToArray(); // 128 bits
// get 15 messages, last byte will indicate if there are more
List<byte[]> msgs = Data.GetMessages(clientPhone, 7) ?? [];
Write(id, $"Got {msgs.Count} messages");
byte[] msgsBytes = new byte[msgs.Select(m => m.Length).Sum()];
int msgsbytesIndex = 0;
for (int i = 0; i < msgs.Count; i += 1)
@ -260,8 +289,11 @@ public class Program
}
msgsLens[15] = Data.PeekMessages(clientPhone) ? (byte)1 : (byte)0;
// only need to encrypt the lengths of the messages, as the messages themselves are encrypted
log.Decrypted("GetMessages", msgsLens);
msgsLens = sk.EncryptCfb(msgsLens, sk.IV, PaddingMode.None);
log.Encrypted("GetMessages", msgsLens);
byte[] finalPayload = [.. msgsLens, .. msgsBytes];
log.Encrypted("GetMessages Final", finalPayload);
await stream.WriteAsync(finalPayload);
break;
case RequestType.GetUserKey:
@ -270,13 +302,17 @@ public class Program
if (key != null)
{
msg = [0, .. key.ExportRSAPublicKey()];
log.Decrypted("GetUserKey", msg);
msg = sk.EncryptCfb(msg, sk.IV, PaddingMode.PKCS7);
log.Encrypted("GetUserKey", msg);
await stream.WriteAsync(msg);
}
else
{
msg = [1, .. Encoding.UTF8.GetBytes("USER DOES NOT EXIST")];
log.Decrypted("GetUserKey", msg);
msg = sk.EncryptCfb(msg, sk.IV, PaddingMode.PKCS7);
log.Encrypted("GetUserKey", msg);
await stream.WriteAsync(msg);
}
break;
@ -285,12 +321,13 @@ public class Program
int msgLen = BitConverter.ToInt32(msg, 11);
if (msgLen != (len - MSG_LEN))
{
Write(id, $"Got message to {recv} of length {len - MSG_LEN} but expected {msgLen}");
log.System($"Got message to {recv} of length {len - MSG_LEN} but expected {msgLen}");
}
byte[] clientMsg = buffer[MSG_LEN..(msgLen + MSG_LEN)];
log.Encrypted("SendMessage message", clientMsg);
// simply add the clientMsg to the "Data"
bool added = Data.AddMessage(recv, clientMsg);
Write(id, $"Added message to {recv} of length {msgLen}: {added}");
log.System($"Added message to {recv} of length {msgLen}: {added}");
break;
default:
msg = sk.EncryptCfb(Encoding.UTF8.GetBytes("INVALID REQUEST"), sk.IV, PaddingMode.PKCS7);
@ -303,8 +340,8 @@ public class Program
}
catch (Exception ex)
{
Write(id, $"Client failed with error {ex.Message}");
Write(id, $"Stack: {ex.StackTrace}");
log.System($"Client failed with error {ex.Message}");
log.System($"Stack: {ex.StackTrace}");
}
client.Dispose();
@ -315,14 +352,8 @@ public class Program
return counter == byte.MaxValue ? (byte)0 : (byte)(counter + 1);
}
static async Task Send6DigitCodeInSecureChannel(NetworkStream stream, byte[] code)
static async Task SendBySecureChannel(NetworkStream stream, byte[] code)
{
await stream.WriteAsync(code);
}
/// Helper log message so it would print both the time and the id
static void Write(int id, string Message)
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {id} - {Message}");
}
}

1
server_data.json Normal file
View file

@ -0,0 +1 @@
{"Keys":{"0000":{"KeyExchangeAlgorithm":"RSA","SignatureAlgorithm":"RSA","KeySize":1024,"LegalKeySizes":[{"MinSize":512,"MaxSize":16384,"SkipSize":8}]}},"Messages":{"0000":["FfjoDxhjwTeAg8+SLXd+JR53uxIWR/I03/vTeagDX14oLqyxMgSoIKmsgDELv9aSOipN0ZcQwSeaYu6EGIZEn33r8bNpDX7rSSaztt9OEFINlAMQAMAzPuKvGB4nDg7MyDBmHXGxlvYLMTM93WoIWb2BWk3Nz/rpd3fB/Wt7/yXoNlK9hTvPbVcFMMgntVs1gjSuHHx/RLrBZ/7aoEkpEB1HM1NkvnAccYQGluv+7FES86xI8AXWIuU3iBDM3LhUUPZxmFWjLToiMqFtyp8BMyRH7HUufbzQpw+BpZMGo33LekitVhG42LHQDebsjdvWUFutLVDDdwG7sPPmzKBYRQ=="]}}