This document describes how snc
works internally so that it feels safer to use for those who want
a TLDR instead of going through the source code.
For encryption, snc
utilizes Rijndael, the AES (Advanced Encryption Standard). The code is a clone
of my own AES implementation. The default key size is set to 128 as
it provides better performance. The key size can be safely altered to 192 or 256 bits, but it should
be noted that bigger isn't necessarily better; larger AES key sizes exist mostly due to regulations,
not because they are harder to break. 128 bits are more than enough for today's technology. That
being said, the AES key size can be changed in src/include/aes.h
.
The SHA3 algorithm is used for key hashing as it allows for flexible digest length and thus enabling the AES key size to be changed without further changes to the source code.
CRC32 is used for packet integrity checking.
Authentication starts with the client to harden against service fingerprinting. If the first message
received from the client has unexpected size, the connection is simply closed. Effectively, snc
's
authentication works in a 3-way handshake and inherently provides mutual authentication.
- Client generates 2 random 16-byte AES IVs and a random 16-byte challenge.
- Client initializes two AES states with it's key and generated IVs. One for encryption and another one for decryption.
- Client encrypts the challenge.
- Client concatenates IVs and challenge and sends the corresponding 48 bytes to the server.
- Server receives IVs and challenge.
- Server initializes two AES states with it's key and received IVs. One for decryption and another one for encryption (the client's encryption state is the server's decryption state and vice-versa).
- Server decrypts the challenge and stores the result.
- Server encrypts the challenge.
- Server sends the challenge to the client.
- Client receives the challenge from the server.
- Client decrypts the challenge and stores the result.
- Client encrypts the challenge.
- Client sends the challenge to the server.
- Server receives the challenge.
- Server decrypts the challenge.
- Both sides now have the necessary information authenticate each other.
Data transfer is a symmetrical process (same routine for both sides) and happens asynchronously. The data polling loop continues until EOF is read on either side.
Below is a snippet from src/include/net.h
describing snc
's packet structure.
// Make header size divisible by AES_SIZE_BLOCK
#define HDR_SIZE_PADDING 8
// Up to 32Kb per packet (also divisible by AES_SIZE_BLOCK)
#define PKT_SIZE_PAYLOAD 32768
// The snc header data
struct SNCHeader {
uint32_t size;
uint32_t crc32;
uint8_t padding[HDR_SIZE_PADDING];
};
// The snc packet data
struct SNCPacket {
struct SNCHeader hdr;
uint8_t payload[PKT_SIZE_PAYLOAD];
};
- Read at most
PKT_SIZE_PAYLOAD
bytes from it's input intosnc_packet.payload
. - Pad
snc_packet.payload
with random bytes until it's size is divisible byAES_SIZE_BLOCK
. - Set
snc_packet.snc_header.size
to the original data size. - Set
snc_packet.snc_header.crc32
to the CRC32 of the original data. - Encrypt
snc_packet.snc_header
. - Encrypt
snc_packet.payload
. - Send packet.
- Read
sizeof(snc_header)
bytes from the socket intosnc_packet.snc_header
. - Decrypt
snc_packet.snc_header
. - Read
snc_packet.snc_header.size
bytes plus padding from the socket intosnc_packet.payload
. - Decrypt
snc_packet.payload
. - Verify the integrity of
snc_packet.payload
by checking it's CRC32 againstsnc_header.crc32
. - If the integrity check fails, close the connection with the appropriate error.