diff --git a/libp2p/protocols/mix/crypto.nim b/libp2p/protocols/mix/crypto.nim new file mode 100644 index 000000000..35757cdda --- /dev/null +++ b/libp2p/protocols/mix/crypto.nim @@ -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] diff --git a/libp2p/protocols/mix/curve25519.nim b/libp2p/protocols/mix/curve25519.nim new file mode 100644 index 000000000..fd43c9d6e --- /dev/null +++ b/libp2p/protocols/mix/curve25519.nim @@ -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) diff --git a/tests/mix/testcrypto.nim b/tests/mix/testcrypto.nim new file mode 100644 index 000000000..b717f9977 --- /dev/null +++ b/tests/mix/testcrypto.nim @@ -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 diff --git a/tests/mix/testcurve25519.nim b/tests/mix/testcurve25519.nim new file mode 100644 index 000000000..f36897fe7 --- /dev/null +++ b/tests/mix/testcurve25519.nim @@ -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 diff --git a/tests/testnative.nim b/tests/testnative.nim index 1212b3ca4..90888375b 100644 --- a/tests/testnative.nim +++ b/tests/testnative.nim @@ -41,3 +41,5 @@ import kademlia/[testencoding, testroutingtable, testfindnode, testputval] when defined(libp2p_autotls_support): import testautotls + +import mix/[testcrypto, testcurve25519]