more progress :D

This commit is contained in:
Rusty Striker 2024-12-17 20:41:30 +02:00
parent d49131bc67
commit c4524fb62e
Signed by: RustyStriker
GPG key ID: 87E4D691632DFF15
6 changed files with 210 additions and 51 deletions

View file

@ -19,6 +19,24 @@
## client todo:
[ ] Check for key when turned on
[ ] generate key and register if no key is preset, and save it after registration is done
[ ] if key is present, start by getting messages (which makes sure we are signed in)
[x] generate key and register if no key is preset, and save it after registration is done
[ ] if key is present, start by establishing connection (which makes sure we are signed in)
[ ] use AES to get basic packets from the server
[ ] use RSA private key to read normal messages
## Server todo:
[ ] Laucnh task for each new connection
[ ] use RSA key to get first message and extract AES key
[ ] verify the user using its public RSA key
[ ] if it was a register session save the key into the BIG DATA STRUCTURE
[ ] Keep lists of incoming messages
(doesnt need to know from who, they are just big blobs of shlomp)
[ ] When user asks for incoming messages, make basic packet and append the incoming messages
- last byte is the "how many messages are left" byte
- each byte in the extra data will be the length of the next message, so
if there are 3 messages of length 128, 200, 300 bytes it will be
[128, 200, 300, 0 ...] and the actual position in the payload is easy
to calculate ([128, 200+128=328, 300+328=628, ...])

View file

@ -4,23 +4,52 @@ using System.Text;
using System.Threading.Tasks;
using System.Security.Cryptography;
using System.Linq;
using System.IO;
using lib;
namespace Client;
public class Program
{
static void Main(string[] args)
static async Task Main(string[] args)
{
string user = args
.SkipWhile(a => !new[] { "-p", "--phone" }.Contains(a)) // search for the option
.Skip(1) // skip the `-u/--user` itself to get the value
.FirstOrDefault() ?? "0000"; // get the value or deafult if it doesnt exist
RSA key = RSA.Create(512);
Console.WriteLine($"key: {key.ExportRSAPrivateKey().Length}");
Console.WriteLine(key.ExportRSAPublicKeyPem());
// On boot, check if a key is available
RSA serverKey = LoadRSAFromFile("key_server.pem")!;
RSA pubKey = LoadRSAFromFile($"pubkey_{user}.pem") ?? RSA.Create(2048);
RSA privKey = LoadRSAFromFile($"privkey_{user}.pem") ?? pubKey;
SaveRSAKeys(user, pubKey, privKey);
using TcpClient client = new("127.0.0.1", 12345);
byte[] toSend = Encoding.ASCII.GetBytes("hello server");
var stream = client.GetStream();
// First contact init
bool needsRegister = pubKey == privKey || args.Any(a => new[] { "-fr", "--force-register" }.Contains(a));
Aes sk = Aes.Create(); // creates an AES-256 key
if (needsRegister)
{
while(true) {
try {
await RegisterClient(user, pubKey, privKey, serverKey, sk, stream);
}
catch (Exception ex) {
Console.WriteLine("Failed registration process");
Console.WriteLine("Exception: " + ex.Message);
Console.WriteLine("Stack: " + ex.StackTrace);
}
}
}
else
{
}
var inputTask = Task.Run(async () => await HandleUserInput(client, stream));
var serverInput = Task.Run(async () => await HandleServerInput(client, stream));
@ -28,34 +57,108 @@ public class Program
}
static async Task HandleUserInput(TcpClient client, NetworkStream stream) {
while(client.Connected) {
async static Task RegisterClient(string user, RSA pub, RSA priv, RSA server, Aes sk, NetworkStream stream)
{
byte counter = 0;
// first send the `Register` message
byte[] pubBytes = pub.ExportRSAPublicKey();
byte[] data = new byte[16];
Array.Copy(Utils.NumberToBytes(user), data, 8);
Array.Copy(BitConverter.GetBytes(pubBytes.Length), 0, data, 8, 4);
Array.Copy(BitConverter.GetBytes(sk.Key.Length), 0, data, 12, 4);
byte[] msg = Request.CreateRequest(RequestType.Register, ref counter, data);
byte[] finalPayload = [.. msg, .. pubBytes, .. sk.Key];
// signing a hash here is pretty useless tbh
// if oscar is changing this message somehow it will just fail the registration process and will restart eventually
// so kinda pointless, esp when its signed with the server's key
byte[] enc = server.Encrypt(finalPayload, RSAEncryptionPadding.OaepSHA256);
await stream.WriteAsync(enc);
// get the 6 digit code
byte[] digits = new byte[6];
await stream.ReadExactlyAsync(digits, 0, 6);
// print the 6 digit code
Console.WriteLine($"[{DateTime.Now}] 6 digit code: {string.Join(' ',digits.Select(d => d.ToString()))}");
// get the 6 digit code from the user
while(true) {
string? code = Console.ReadLine()?.Trim();
if(code == null || code.Take(6).Any(c => !char.IsDigit(c))) {
Console.WriteLine("Invalid code!");
continue;
}
byte[] codeBytes = code
.Take(6) // take the first 6 characters
.Select(d => byte.Parse(d.ToString())) // parse into bytes
.ToArray();
msg = Request.CreateRequest(RequestType.ConfirmRegister, ref counter, codeBytes);
byte[] signed = priv.SignData(msg, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
enc = sk.EncryptCfb(msg, Array.Empty<byte>(), PaddingMode.None); // no reason to encrpy the signature
finalPayload = [.. enc, .. signed]; // should be 128 (enc) + 256 (signed)
await stream.WriteAsync(finalPayload);
int incoming = await stream.ReadAsync(enc);
msg = sk.DecryptCfb(enc[..incoming], Array.Empty<byte>(), PaddingMode.Zeros);
string r = Encoding.UTF8.GetString(msg);
if(r == "OK") {
Console.WriteLine("Registration process complete");
break;
}
}
}
static RSA? LoadRSAFromFile(string file)
{
if (File.Exists(file))
{
string content = File.ReadAllText(file);
RSA k = RSA.Create();
k.ImportFromPem(content);
return k;
}
return null;
}
static void SaveRSAKeys(string user, RSA pub, RSA priv)
{
File.WriteAllText($"pubkey_{user}.pem", pub.ExportRSAPublicKeyPem());
File.WriteAllText($"privkey_{user}.pem", priv.ExportRSAPrivateKeyPem());
}
static async Task HandleUserInput(TcpClient client, NetworkStream stream)
{
while (client.Connected)
{
string? input = Console.ReadLine();
if(input == null) {
if (input == null)
{
await Task.Delay(100);
continue;
}
else if(input.StartsWith('/')) {
else if (input.StartsWith('/'))
{
// Commands :D, i like commands
switch(input.ToLower()) {
switch (input.ToLower())
{
case "/quit":
case "/exit":
case "/q!":
return;
}
}
else {
else
{
stream.Write(Encoding.ASCII.GetBytes(input));
Console.WriteLine($"[{DateTime.Now}]Sent to server: {input}");
}
}
}
static async Task HandleServerInput(TcpClient client, NetworkStream stream) {
static async Task HandleServerInput(TcpClient client, NetworkStream stream)
{
byte[] buffer = new byte[1024];
while(client.Connected) {
while (client.Connected)
{
int readLen = await stream.ReadAsync(buffer);
if(readLen != 0) {
if (readLen != 0)
{
string fromServer = Encoding.ASCII.GetString(buffer[..readLen]);
Console.WriteLine($"[{DateTime.Now}] From server: {fromServer}");

View file

@ -1,11 +0,0 @@
using System.Security.Cryptography;
namespace lib;
public static class Crypto {
public static RSA GenerateKey() {
return RSA.Create(512);
}
}

View file

@ -3,8 +3,31 @@
public enum RequestType {
Register = 1,
ConfirmRegister = 2,
GetMessages = 3,
GetUserKey = 4,
SendMessage = 5,
SendAck = 6
Login = 3,
GetMessages = 4,
GetUserKey = 5,
SendMessage = 6,
}
public static class Request {
public static byte[] CreateRequest(RequestType Type, ref byte counter, byte[] data) {
if(data.Length > 13) {
throw new Exception("extra data is too long");
}
byte[] msg = new byte[128];
msg[0] = 0; // version
msg[1] = (byte)Type;
msg[2] = counter;
if(counter == byte.MaxValue) {
counter = 0;
}
else {
counter += 1;
}
// insert data 3..16
Array.Copy(data, 0, msg, 3, data.Length);
return msg;
}
}

View file

@ -48,32 +48,30 @@ to the server.
## "Control" requests
- Register `(phone, pub RSA key)`
extra data: RSA key size (payload length)
- Register:
data: Phone - 8 bytes, RSA key size (payload length) - 2 bytes, AES key length - 2 bytes
- ConfirmRegister (signed & encrypted 6 digit code)
extra data: 6 bytes for the 6 digit code (can use less but it will be padding otherwise)
- GetMessages (signed & encrypted to not allow someone to "flush" someone else's msgs)
extra data: EMPTY
data: 6 bytes for the 6 digit code (can use less but it will be padding otherwise)
- Login (signed hash):
data: 8 bytes of user's phone, AES key length - 2 bytes, signed SHA length - 2 bytes
- GetMessages:
data: EMPTY
- GetUserKey (dont think it needs any signing or encryption technically, as it is very
simple registering and "stealing" a key outside the system, keys are assumed to be
unbreakable)
extra data: 8 bytes (4 bits per digit) of whoever we want to get the key of
- SendMessage `(to_user, msg_id, EM from d above)`
extra data: 8 bytes (4 bits per digit) of who to send the data, 4 bytes (32bit) for length
- SendAck `(to_user, msg_id - signed and encrypted)`
- server doesnt know if the msg itself is indeed valid and not tempered with
extra data: 8 bytes (4 bits per digit) of who to send the data, 4 bytes (32bit) for length
extra data: 8 bytes (4 bits per digit) of who to send the data, 4 bytes (32bit) for length in bytes
I think it all can go into a:
```
{
Version byte (0) - 1 byte
RequestType - 1 byte,
Phone number - 8 bytes (every 4 bits is a number, so we have 16 numbers),
UTC timestamp - 8 bytes (long),
extra data (based on the request given) - up to 18 bytes
} = 32 bytes = 256 bits
looping counter - 1 bytes (long), could be replaced by a counter, that can be 1 looping byte, thus
data - up to 13,
} = 16 bytes = 128 bits
```
and we can just append encrypted payloads to it (in SendMessage and SendAck)
To each message we also append a sha3-256 signed hash, this shit really feels like overkill and im not sure about it,
@ -83,5 +81,26 @@ amusingly long for the amount of data the server needs to encrypt
enc_server( request - 256 bits, signed sha3-256 )
which means:
Keys: RSA-512
public keys: RSA-2048/1024 (not sure what i need here honestly)
Hashes: SHA3-256
symmetric keys: AES-256
scrape that... seems it wouldnt work as expected since it doesnt work that way
with RSA i think maybe not im not sure...
thinking right now about an RSA-2048 key (for sEcUrItY),
then starting a connection will be whatever the user wants, and optionally
an AES-128 (because its only for 1 session) key to use for subsequent messages,
this allows the user to NOT encrypt everything with RSA keys, it still means the
server has to decrypt and verify a signature for the first message (and either return
an error or force close the connection if the sig is invalid and all)
this somewhat applies for the register messages, you send a payload with an AES key
and a public RSA key, then in `ConfirmRegister` the key is still being signed with
the user's RSA, but then encrypted using the AES session key.
Messages between users will still be done using public RSA keys,
which honestly can be both 1024, or 2048 as far as im aware, prob should also be
2048, that means the server needs to do 2 RSA encryptions per session
(or 2 for the registration itself, which is then auto-validating the session)

View file

@ -1,6 +1,8 @@
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
namespace Server;
@ -8,6 +10,11 @@ public class Program
{
static void Main(string[] args)
{
// Generally this key would be static but since its not production yet we can generate it every time to make sure
// the users has the key and could load it from file
RSA key = RSA.Create(2048);
File.WriteAllText("server_key.pem", key.ExportRSAPublicKeyPem());
int port = 12345;
TcpListener server = new(IPAddress.Parse("0.0.0.0"), port);
try {