feat(mix): crypto (#1687)

This commit is contained in:
richΛrd
2025-09-15 12:46:28 -04:00
committed by GitHub
parent d8ecf8a135
commit 0751f240a2
5 changed files with 277 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
import endians, nimcrypto
proc aes_ctr*(key, iv, data: openArray[byte]): seq[byte] =
## Processes 'data' using AES in CTR mode.
## For CTR mode, the same function handles both encryption and decryption.
doAssert key.len == 16, "Key must be 16 bytes for AES-128"
doAssert iv.len == 16, "IV must be 16 bytes for AES-128"
var
ctx: CTR[aes128]
output = newSeq[byte](data.len)
ctx.init(key, iv)
ctx.encrypt(data, output)
ctx.clear()
output
proc advance_ctr*(iv: var openArray[byte], blocks: uint64) =
## Advances the counter in the AES-CTR IV by a specified number of blocks.
var counter: uint64
bigEndian64(addr counter, addr iv[8])
counter += blocks
bigEndian64(addr iv[8], addr counter)
proc aes_ctr_start_index*(key, iv, data: openArray[byte], startIndex: int): seq[byte] =
## Encrypts 'data' using AES in CTR mode from startIndex, without processing all preceding data.
## For CTR mode, the same function handles both encryption and decryption.
doAssert key.len == 16, "Key must be 16 bytes for AES-128"
doAssert iv.len == 16, "IV must be 16 bytes for AES-128"
doAssert startIndex mod 16 == 0, "Start index must be a multiple of 16"
var advIV = @iv
# Advance the counter to the start index
let blocksToAdvance = startIndex div 16
advance_ctr(advIV, blocksToAdvance.uint64)
return aes_ctr(key, advIV, data)
proc sha256_hash*(data: openArray[byte]): array[32, byte] =
## hashes 'data' using SHA-256.
return sha256.digest(data).data
proc kdf*(key: openArray[byte]): seq[byte] =
## Returns the hash of 'key' truncated to 16 bytes.
let hash = sha256_hash(key)
return hash[0 .. 15]
proc hmac*(key, data: openArray[byte]): seq[byte] =
## Computes a HMAC for 'data' using given 'key'.
let hmac = sha256.hmac(key, data).data
return hmac[0 .. 15]

View File

@@ -0,0 +1,52 @@
import results
import bearssl/rand
import ../../crypto/curve25519
const FieldElementSize* = Curve25519KeySize
type FieldElement* = Curve25519Key
proc bytesToFieldElement*(bytes: openArray[byte]): Result[FieldElement, string] =
## Convert bytes to FieldElement
if bytes.len != FieldElementSize:
return err("Field element size must be 32 bytes")
ok(intoCurve25519Key(bytes))
proc fieldElementToBytes*(fe: FieldElement): seq[byte] =
## Convert FieldElement to bytes
fe.getBytes()
# Generate a random FieldElement
proc generateRandomFieldElement*(): Result[FieldElement, string] =
let rng = HmacDrbgContext.new()
if rng.isNil:
return err("Failed to creat HmacDrbgContext with system randomness")
ok(Curve25519Key.random(rng[]))
# Generate a key pair (private key and public key are both FieldElements)
proc generateKeyPair*(): Result[tuple[privateKey, publicKey: FieldElement], string] =
let privateKey = generateRandomFieldElement().valueOr:
return err("Error in private key generation: " & error)
let publicKey = public(privateKey)
ok((privateKey, publicKey))
proc multiplyPointWithScalars*(
point: FieldElement, scalars: openArray[FieldElement]
): FieldElement =
## Multiply a given Curve25519 point with a set of scalars
var res = point
for scalar in scalars:
Curve25519.mul(res, scalar)
res
proc multiplyBasePointWithScalars*(
scalars: openArray[FieldElement]
): Result[FieldElement, string] =
## Multiply the Curve25519 base point with a set of scalars
if scalars.len <= 0:
return err("Atleast one scalar must be provided")
var res: FieldElement = public(scalars[0]) # Use the predefined base point
for i in 1 ..< scalars.len:
Curve25519.mul(res, scalars[i]) # Multiply with each scalar
ok(res)

125
tests/mix/testcrypto.nim Normal file
View File

@@ -0,0 +1,125 @@
{.used.}
import nimcrypto, results, unittest
import ../../libp2p/protocols/mix/crypto
suite "cryptographic_functions_tests":
test "aes_ctr_encrypt_decrypt":
let
key = cast[array[16, byte]]("thisis16byteskey")
iv = cast[array[16, byte]]("thisis16bytesiv!")
data: seq[byte] = cast[seq[byte]]("thisisdata")
let encrypted = aes_ctr(key, iv, data)
let decrypted = aes_ctr(key, iv, encrypted)
check:
data == decrypted
data != encrypted
test "sha256_hash_computation":
let
data: seq[byte] = cast[seq[byte]]("thisisdata")
expectedHashHex =
"b53a20ecf0814267a83be82f941778ffda4b85fbf93a07847539f645ff5f1b9b"
expectedHash = fromHex(expectedHashHex)
hash = sha256_hash(data)
check hash == expectedHash
test "kdf_computation":
let
key: seq[byte] = cast[seq[byte]]("thisiskey")
expectedKdfHex = "37c9842d37dc404854428a0a3554dcaa"
expectedKdf = fromHex(expectedKdfHex)
derivedKey = kdf(key)
check derivedKey == expectedKdf
test "hmac_computation":
let
key: seq[byte] = cast[seq[byte]]("thisiskey")
data: seq[byte] = cast[seq[byte]]("thisisdata")
expectedHmacHex = "b075dd302655e085d35e8cef5dfdf101"
expectedHmac = fromHex(expectedHmacHex)
hmacResult = hmac(key, data)
check hmacResult == expectedHmac
test "aes_ctr_empty_data":
let
key = cast[array[16, byte]]("thisis16byteskey")
iv = cast[array[16, byte]]("thisis16bytesiv!")
emptyData: array[0, byte] = []
let encrypted = aes_ctr(key, iv, emptyData)
let decrypted = aes_ctr(key, iv, encrypted)
check:
emptyData == decrypted
emptyData == encrypted
test "sha256_hash_empty_data":
let
emptyData: array[0, byte] = []
expectedHashHex =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
expectedHash = fromHex(expectedHashHex)
hash = sha256_hash(emptyData)
check hash == expectedHash
test "kdf_empty_key":
let
emptyKey: array[0, byte] = []
expectedKdfHex = "e3b0c44298fc1c149afbf4c8996fb924"
expectedKdf = fromHex(expectedKdfHex)
derivedKey = kdf(emptyKey)
check derivedKey == expectedKdf
test "hmac_empty_key_and_data":
let
emptyKey: array[0, byte] = []
emptyData: array[0, byte] = []
expectedHmacHex = "b613679a0814d9ec772f95d778c35fc5"
expectedHmac = fromHex(expectedHmacHex)
hmacResult = hmac(emptyKey, emptyData)
check hmacResult == expectedHmac
test "aes_ctr_start_index_zero_index":
let
key = cast[array[16, byte]]("thisis16byteskey")
iv = cast[array[16, byte]]("thisis16bytesiv!")
data: seq[byte] = cast[seq[byte]]("thisisdata")
startIndex = 0
let encrypted = aes_ctr_start_index(key, iv, data, startIndex)
let expected = aes_ctr(key, iv, data)
check encrypted == expected
test "aes_ctr_start_index_empty_data":
let
key = cast[array[16, byte]]("thisis16byteskey")
iv = cast[array[16, byte]]("thisis16bytesiv!")
emptyData: array[0, byte] = []
startIndex = 0
let encrypted = aes_ctr_start_index(key, iv, emptyData, startIndex)
check emptyData == encrypted
test "aes_ctr_start_index_middle":
let
key = cast[array[16, byte]]("thisis16byteskey")
iv = cast[array[16, byte]]("thisis16bytesiv!")
data: seq[byte] = cast[seq[byte]]("thisisverylongdata")
startIndex = 16
let encrypted2 = aes_ctr_start_index(key, iv, data[startIndex ..^ 1], startIndex)
let encrypted1 = aes_ctr(key, iv, data[0 .. startIndex - 1])
let expected = aes_ctr(key, iv, data)
check encrypted1 & encrypted2 == expected

View File

@@ -0,0 +1,45 @@
{.used.}
import results, unittest
import ../../libp2p/crypto/curve25519
import ../../libp2p/protocols/mix/curve25519
proc isNotZero(key: FieldElement): bool =
for byte in key:
if byte != 0:
return true
return false
suite "curve25519_tests":
test "generate_key_pair":
let (privateKey, publicKey) = generateKeyPair().expect("generate keypair error")
check:
fieldElementToBytes(privateKey).len == FieldElementSize
fieldElementToBytes(publicKey).len == FieldElementSize
privateKey.isNotZero()
publicKey.isNotZero()
let derivedPublicKey = multiplyBasePointWithScalars(@[privateKey]).expect(
"multiply base point with scalar error"
)
check publicKey == derivedPublicKey
test "commutativity":
let
x1 = generateRandomFieldElement().expect("generate random field element error")
x2 = generateRandomFieldElement().expect("generate random field element error")
res1 = multiplyBasePointWithScalars(@[x1, x2]).expect(
"multiply base point with scalar errors"
)
res2 = multiplyBasePointWithScalars(@[x2, x1]).expect(
"multiply base point with scalar errors"
)
res3 = multiplyPointWithScalars(public(x2), @[x1])
res4 = multiplyPointWithScalars(public(x1), @[x2])
check:
res1 == res2
res1 == res3
res1 == res4

View File

@@ -41,3 +41,5 @@ import kademlia/[testencoding, testroutingtable, testfindnode, testputval]
when defined(libp2p_autotls_support):
import testautotls
import mix/[testcrypto, testcurve25519]