Implemented AES256-GCM encryption of all network packets. Requires some more refactoring to remove redundant code and make it cleaner.

This commit is contained in:
Jakob Friedl
2025-07-23 13:47:37 +02:00
parent 36719dd7f0
commit 0f065f41a2
16 changed files with 298 additions and 207 deletions

View File

@@ -0,0 +1,48 @@
import random
import nimcrypto
import ./[utils, types]
proc generateSessionKey*(): Key =
# Generate a random 256-bit (32-byte) session key for AES-256 encryption
var key: array[32, byte]
for i in 0 ..< 32:
key[i] = byte(rand(255))
return key
proc generateIV*(): Iv =
# Generate a random 98-bit (12-byte) initialization vector for AES-256 GCM mode
var iv: array[12, byte]
for i in 0 ..< 12:
iv[i] = byte(rand(255))
return iv
proc encrypt*(key: Key, iv: Iv, data: seq[byte], sequenceNumber: uint64): (seq[byte], AuthenticationTag) =
# Encrypt data using AES-256 GCM
var encData = newSeq[byte](data.len)
var tag: AuthenticationTag
var ctx: GCM[aes256]
ctx.init(key, iv, sequenceNumber.toBytes())
ctx.encrypt(data, encData)
ctx.getTag(tag)
ctx.clear()
return (encData, tag)
proc decrypt*(key: Key, iv: Iv, encData: seq[byte], sequenceNumber: uint64): (seq[byte], AuthenticationTag) =
# Decrypt data using AES-256 GCM
var data = newSeq[byte](encData.len)
var tag: AuthenticationTag
var ctx: GCM[aes256]
ctx.init(key, iv, sequenceNumber.toBytes())
ctx.decrypt(encData, data)
ctx.getTag(tag)
ctx.clear()
return (data, tag)

View File

@@ -99,6 +99,39 @@ proc getBytes*(unpacker: Unpacker, length: int): seq[byte] =
if bytesRead != length:
raise newException(IOError, "Not enough data to read")
proc getKey*(unpacker: Unpacker): Key =
var key: Key
let bytesRead = unpacker.stream.readData(key[0].unsafeAddr, 32)
unpacker.position += bytesRead
if bytesRead != 32:
raise newException(IOError, "Not enough data to read key")
return key
proc getIv*(unpacker: Unpacker): Iv =
var iv: Iv
let bytesRead = unpacker.stream.readData(iv[0].unsafeAddr, 12)
unpacker.position += bytesRead
if bytesRead != 12:
raise newException(IOError, "Not enough data to read IV")
return iv
proc getAuthenticationTag*(unpacker: Unpacker): AuthenticationTag =
var tag: AuthenticationTag
let bytesRead = unpacker.stream.readData(tag[0].unsafeAddr, 16)
unpacker.position += bytesRead
if bytesRead != 16:
raise newException(IOError, "Not enough data to read authentication tag")
return tag
proc getArgument*(unpacker: Unpacker): TaskArg =
result.argType = unpacker.getUint8()
@@ -133,8 +166,23 @@ proc packHeader*(packer: Packer, header: Header, bodySize: uint32): seq[byte] =
.add(header.version)
.add(header.packetType)
.add(header.flags)
.add(header.seqNr)
.add(bodySize)
.addData(header.hmac)
.add(header.agentId)
.add(header.seqNr)
.addData(header.iv)
.addData(header.gmac)
return packer.pack()
return packer.pack()
proc unpackHeader*(unpacker: Unpacker): Header=
return Header(
magic: unpacker.getUint32(),
version: unpacker.getUint8(),
packetType: unpacker.getUint8(),
flags: unpacker.getUint16(),
size: unpacker.getUint32(),
agentId: unpacker.getUint32(),
seqNr: unpacker.getUint64(),
iv: unpacker.getIv(),
gmac: unpacker.getAuthenticationTag()
)

View File

@@ -7,7 +7,7 @@ import streams
const
MAGIC* = 0x514E3043'u32 # Magic value: C0NQ
VERSION* = 1'u8 # Version 1
HEADER_SIZE* = 32'u8 # 32 bytes fixed packet header size
HEADER_SIZE* = 52'u8 # 48 bytes fixed packet header size
type
PacketType* = enum
@@ -49,14 +49,24 @@ type
RESULT_BINARY = 1'u8
RESULT_NO_OUTPUT = 2'u8
# Encryption
type
Key* = array[32, byte]
Iv* = array[12, byte]
AuthenticationTag* = array[16, byte]
# Packet structure
type
Header* = object
magic*: uint32 # [4 bytes ] magic value
version*: uint8 # [1 byte ] protocol version
packetType*: uint8 # [1 byte ] message type
flags*: uint16 # [2 bytes ] message flags
seqNr*: uint32 # [4 bytes ] sequence number / nonce
size*: uint32 # [4 bytes ] size of the payload body
hmac*: array[16, byte] # [16 bytes] hmac for message integrity
magic*: uint32 # [4 bytes ] magic value
version*: uint8 # [1 byte ] protocol version
packetType*: uint8 # [1 byte ] message type
flags*: uint16 # [2 bytes ] message flags
size*: uint32 # [4 bytes ] size of the payload body
agentId*: uint32 # [4 bytes ] agent id, used as AAD for encryptio
seqNr*: uint64 # [8 bytes ] sequence number, used as AAD for encryption
iv*: Iv # [12 bytes] random IV for AES256 GCM encryption
gmac*: AuthenticationTag # [16 bytes] authentication tag for AES256 GCM encryption
TaskArg* = object
argType*: uint8 # [1 byte ] argument type
@@ -66,7 +76,6 @@ type
header*: Header
taskId*: uint32 # [4 bytes ] task id
agentId*: uint32 # [4 bytes ] agent id
listenerId*: uint32 # [4 bytes ] listener id
timestamp*: uint32 # [4 bytes ] unix timestamp
command*: uint16 # [2 bytes ] command id
@@ -77,7 +86,6 @@ type
header*: Header
taskId*: uint32 # [4 bytes ] task id
agentId*: uint32 # [4 bytes ] agent id
listenerId*: uint32 # [4 bytes ] listener id
timestamp*: uint32 # [4 bytes ] unix timestamp
command*: uint16 # [2 bytes ] command id
@@ -104,16 +112,14 @@ type
# Checkin binary structure
type
Heartbeat* = object
header*: Header
agentId*: uint32 # [4 bytes ] agent id
listenerId*: uint32 # [4 bytes ] listener id
timestamp*: uint32
header*: Header # [48 bytes ] fixed header
listenerId*: uint32 # [4 bytes ] listener id
timestamp*: uint32 # [4 bytes ] unix timestamp
# Registration binary structure
type
# All variable length fields are stored as seq[byte], prefixed with 4 bytes indicating the length of the following data
AgentMetadata* = object
agentId*: uint32
listenerId*: uint32
username*: seq[byte]
hostname*: seq[byte]
@@ -127,7 +133,7 @@ type
AgentRegistrationData* = object
header*: Header
# encMaterial*: seq[byte] # Encryption material for the agent registration
sessionKey*: Key # [32 bytes ] AES 256 session key
metadata*: AgentMetadata
# Agent structure
@@ -148,6 +154,7 @@ type
tasks*: seq[Task]
firstCheckin*: DateTime
latestCheckin*: DateTime
sessionKey*: Key
# Listener structure
type
@@ -176,4 +183,5 @@ type
listenerId*: string
ip*: string
port*: int
sleep*: int
sleep*: int
sessionKey*: Key

View File

@@ -50,4 +50,16 @@ proc toBytes*(value: uint32): seq[byte] =
byte((value shr 8) and 0xFF),
byte((value shr 16) and 0xFF),
byte((value shr 24) and 0xFF)
]
proc toBytes*(value: uint64): seq[byte] =
return @[
byte(value and 0xFF),
byte((value shr 8) and 0xFF),
byte((value shr 16) and 0xFF),
byte((value shr 24) and 0xFF),
byte((value shr 32) and 0xFF),
byte((value shr 40) and 0xFF),
byte((value shr 48) and 0xFF),
byte((value shr 56) and 0xFF)
]