Implemented ECDH key exchange using ed25519 to share a symmetric AES key without transmitting it over the network.

This commit is contained in:
Jakob Friedl
2025-07-24 15:31:46 +02:00
parent cf4e4a7017
commit b6c720ccca
11 changed files with 166 additions and 45 deletions

View File

@@ -1,21 +1,29 @@
import random
import system
import nimcrypto
import nimcrypto/blake2
from ed25519 import keyExchange, createKeyPair, seed
# from monocypher import crypto_key_exchange_public_key, crypto_key_exchange, crypto_blake2b, crypto_wipe
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
#[
Symmetric AES256 GCM encryption for secure C2 traffic
Ensures both confidentiality and integrity of the packet
]#
proc generateKeyPair*(): KeyPair =
let keyPair = createKeyPair(seed())
return KeyPair(
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey
)
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
var iv: Iv
if randomBytes(iv) != 12:
raise newException(CatchableError, "Failed to generate IV.")
return iv
proc encrypt*(key: Key, iv: Iv, data: seq[byte], sequenceNumber: uint64): (seq[byte], AuthenticationTag) =
@@ -47,3 +55,67 @@ proc decrypt*(key: Key, iv: Iv, encData: seq[byte], sequenceNumber: uint64): (se
return (data, tag)
#[
ECDHE key exchange using ed25519
]#
proc loadKeys*(privateKeyFile, publicKeyFile: string): KeyPair =
let filePrivate = open(privateKeyFile, fmRead)
defer: filePrivate.close()
var privateKey: PrivateKey
var bytesRead = filePrivate.readBytes(privateKey, 0, sizeof(PrivateKey))
if bytesRead != sizeof(PrivateKey):
raise newException(ValueError, "Invalid private key length.")
let filePublic = open(publicKeyFile, fmRead)
defer: filePublic.close()
var publicKey: PublicKey
bytesRead = filePublic.readBytes(publicKey, 0, sizeof(PublicKey))
if bytesRead != sizeof(PublicKey):
raise newException(ValueError, "Invalid public key length.")
return KeyPair(
privateKey: privateKey,
publicKey: publicKey
)
proc writeKey*[T: PublicKey | PrivateKey](keyFile: string, key: T) =
let file = open(keyFile, fmWrite)
defer: file.close()
let bytesWritten = file.writeBytes(key, 0, sizeof(T))
if bytesWritten != sizeof(T):
raise newException(ValueError, "Invalid key length.")
proc combineKeys(publicKey, otherPublicKey: Key): Key =
# XOR is a commutative operation, that ensures that the order of the public keys does not matter
for i in 0..<32:
result[i] = publicKey[i] xor otherPublicKey[i]
proc deriveSessionKey*(keyPair: KeyPair, publicKey: Key): Key =
var key: Key
# Calculate shared secret (https://monocypher.org/manual/x25519)
let sharedSecret = keyExchange(publicKey, keyPair.privateKey)
# Add combined public keys to hash
let combinedKeys: Key = combineKeys(keyPair.publicKey, publicKey)
# Calculate Blake2b hash to derive session key
var ctx: blake2_512
ctx.init()
ctx.update(sharedSecret)
ctx.update("CONQUEST".toBytes() & @combinedKeys)
let hash = ctx.finish
let bytes = hash.data[0..<sizeof(Key)]
copyMem(key[0].addr, bytes[0].addr, sizeof(Key))
# Cleanup
zeroMem(sharedSecret[0].addr, sharedSecret.len)
return key

View File

@@ -52,6 +52,8 @@ type
# Encryption
type
Key* = array[32, byte]
PublicKey* = array[32, byte]
PrivateKey* = array[64, byte]
Iv* = array[12, byte]
AuthenticationTag* = array[16, byte]
@@ -133,7 +135,7 @@ type
AgentRegistrationData* = object
header*: Header
sessionKey*: Key # [32 bytes ] AES 256 session key
agentPublicKey*: Key # [32 bytes ] Public key of the connecting agent for key exchange
metadata*: AgentMetadata
# Agent structure
@@ -168,12 +170,17 @@ type
# Server structure
type
KeyPair* = object
privateKey*: PrivateKey
publicKey*: Key
Conquest* = ref object
prompt*: Prompt
dbPath*: string
listeners*: Table[string, Listener]
agents*: Table[string, Agent]
interactAgent*: Agent
keyPair*: KeyPair
# Agent Config
type
@@ -183,4 +190,5 @@ type
ip*: string
port*: int
sleep*: int
sessionKey*: Key
sessionKey*: Key
agentPublicKey*: PublicKey

View File

@@ -1,8 +1,14 @@
import strutils, sequtils, random, strformat
import strutils, sequtils, strformat
import nimcrypto
import ./types
proc generateUUID*(): string =
# Create a 4-byte HEX UUID string (8 characters)
(0..<4).mapIt(rand(255)).mapIt(fmt"{it:02X}").join()
var uuid: array[4, byte]
if randomBytes(uuid) != 4:
raise newException(CatchableError, "Failed to generate UUID.")
return uuid.toHex().toUpperAscii()
proc uuidToUint32*(uuid: string): uint32 =
return fromHex[uint32](uuid)
@@ -62,4 +68,10 @@ proc toBytes*(value: uint64): seq[byte] =
byte((value shr 40) and 0xFF),
byte((value shr 48) and 0xFF),
byte((value shr 56) and 0xFF)
]
]
proc toKey*(value: string): Key =
if value.len != 32:
raise newException(ValueError, "Invalid key length.")
copyMem(result[0].addr, value[0].unsafeAddr, 32)