--[=[
	Cryptography library: Base64

	Return type: buffer
	Example usage:
		local Input = buffer.fromstring("Hello World")
		local Encoded = Base64.Encode(Input)
		local Decoded = Base64.Decode(Encoded)
--]=]

--!strict
--!optimize 2
--!native

local PADDING_CHARACTER = 61
local ALPHABET_BYTES = buffer.create(64) do
	local Characters = {
		65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80,
		81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
		97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,
		113, 114, 115, 116, 117, 118, 119, 120, 121, 122,
		48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
		43, 47
	}

	for Index = 1, 64 do
		buffer.writeu8(ALPHABET_BYTES, Index - 1, Characters[Index])
	end
end

local ENCODE_A, ENCODE_B = buffer.create(131072), buffer.create(131072) do
	for Byte0 = 0, 255 do
		local Sextet0 = Byte0 // 4
		local Sextet0Char = buffer.readu8(ALPHABET_BYTES, Sextet0)
		local Byte0Low = Byte0 % 4
		for Byte1 = 0, 255 do
			local Sextet1 = Byte0Low * 16 + (Byte1 // 16)
			local Index = (Byte0 + Byte1 * 256) * 2
			buffer.writeu16(ENCODE_A, Index, Sextet0Char + buffer.readu8(ALPHABET_BYTES, Sextet1) * 256)
		end
	end

	for Byte1 = 0, 255 do
		local Nibble = (Byte1 % 16) * 4
		for Byte2 = 0, 255 do
			local Sextet2 = Nibble + (Byte2 // 64)
			local Sextet3 = Byte2 % 64
			local Index = (Byte1 + Byte2 * 256) * 2
			buffer.writeu16(ENCODE_B, Index, buffer.readu8(ALPHABET_BYTES, Sextet2) + buffer.readu8(ALPHABET_BYTES, Sextet3) * 256)
		end
	end
end

local DECODE_TET = buffer.create(256) do
	for Index = 1, 64 do
		buffer.writeu8(DECODE_TET, buffer.readu8(ALPHABET_BYTES, Index - 1), Index - 1)
	end

	buffer.writeu8(DECODE_TET, PADDING_CHARACTER, 254)
end

local DECODE_PAIR = buffer.create(131072) do
	for Char0 = 0, 255 do
		local Decoded0 = buffer.readu8(DECODE_TET, Char0)
		for Char1 = 0, 255 do
			local Decoded1 = buffer.readu8(DECODE_TET, Char1)
			local Key = (Char0 + Char1 * 256) * 2
			if Decoded0 < 64 and Decoded1 < 64 then
				buffer.writeu16(DECODE_PAIR, Key, Decoded0 * 64 + Decoded1)
			elseif Decoded0 == 254 or Decoded1 == 254 then
				buffer.writeu16(DECODE_PAIR, Key, 254)
			end
		end
	end
end

local PAIR_A_BYTE1_HIGH = buffer.create(4096)
local PAIR_B_BYTE1_LOW = buffer.create(4096)

local PAIR_A_BYTE0, PAIR_B_BYTE2 = buffer.create(4096), buffer.create(4096) do
	for PairA = 0, 4095 do
		local Sextet0 = PairA // 64
		local Sextet1 = PairA % 64
		buffer.writeu8(PAIR_A_BYTE0, PairA, Sextet0 * 4 + (Sextet1 // 16))
		buffer.writeu8(PAIR_A_BYTE1_HIGH, PairA, (Sextet1 % 16) * 16)
	end

	for PairB = 0, 4095 do
		local Sextet2 = PairB // 64
		local Sextet3 = PairB % 64
		buffer.writeu8(PAIR_B_BYTE1_LOW, PairB, Sextet2 // 4)
		buffer.writeu8(PAIR_B_BYTE2, PairB, (Sextet2 % 4) * 64 + Sextet3)
	end
end

local function Encode(Input: buffer): buffer
	local InputLength = buffer.len(Input)
	if InputLength == 0 then
		return buffer.create(0)
	end

	local Output = buffer.create(((InputLength + 2) // 3) * 4)
	local InputIndex = 0
	local OutputIndex = 0
	local FullTripletEnd = InputLength - (InputLength % 3)
	local UnrollStop = InputLength - 13

	local EncodeA = ENCODE_A
	local EncodeB = ENCODE_B
	local AlphabetBytes = ALPHABET_BYTES

	while InputIndex <= UnrollStop do
		local Word0 = buffer.readu32(Input, InputIndex)
		local Word1 = buffer.readu32(Input, InputIndex + 3)
		local Word2 = buffer.readu32(Input, InputIndex + 6)
		local Word3 = buffer.readu32(Input, InputIndex + 9)

		local KeyA0 = Word0 % 65536
		local KeyB0 = (Word0 // 256) % 65536
		buffer.writeu32(Output, OutputIndex, buffer.readu16(EncodeA, KeyA0 * 2) + buffer.readu16(EncodeB, KeyB0 * 2) * 0x10000)

		local KeyA1 = Word1 % 65536
		local KeyB1 = (Word1 // 256) % 65536
		buffer.writeu32(Output, OutputIndex + 4, buffer.readu16(EncodeA, KeyA1 * 2) + buffer.readu16(EncodeB, KeyB1 * 2) * 0x10000)

		local KeyA2 = Word2 % 65536
		local KeyB2 = (Word2 // 256) % 65536
		buffer.writeu32(Output, OutputIndex + 8, buffer.readu16(EncodeA, KeyA2 * 2) + buffer.readu16(EncodeB, KeyB2 * 2) * 0x10000)

		local KeyA3 = Word3 % 65536
		local KeyB3 = (Word3 // 256) % 65536
		buffer.writeu32(Output, OutputIndex + 12, buffer.readu16(EncodeA, KeyA3 * 2) + buffer.readu16(EncodeB, KeyB3 * 2) * 0x10000)

		InputIndex += 12
		OutputIndex += 16
	end

	while InputIndex < FullTripletEnd do
		local KeyA = buffer.readu16(Input, InputIndex)
		local Byte2 = buffer.readu8(Input, InputIndex + 2)
		local KeyB = (KeyA // 256) + Byte2 * 256
		buffer.writeu32(Output, OutputIndex, buffer.readu16(EncodeA, KeyA * 2) + buffer.readu16(EncodeB, KeyB * 2) * 0x10000)
		InputIndex += 3
		OutputIndex += 4
	end

	local Remainder = InputLength - InputIndex
	if Remainder == 1 then
		local Byte0 = buffer.readu8(Input, InputIndex)
		local Sextet0 = Byte0 // 4
		local Sextet1 = (Byte0 % 4) * 16
		local Pair = buffer.readu8(AlphabetBytes, Sextet0) + buffer.readu8(AlphabetBytes, Sextet1) * 256
		buffer.writeu16(Output, OutputIndex, Pair)
		buffer.writeu8(Output, OutputIndex + 2, PADDING_CHARACTER)
		buffer.writeu8(Output, OutputIndex + 3, PADDING_CHARACTER)
	elseif Remainder == 2 then
		local Byte0 = buffer.readu8(Input, InputIndex)
		local Byte1 = buffer.readu8(Input, InputIndex + 1)
		local KeyA = Byte0 + Byte1 * 256
		buffer.writeu16(Output, OutputIndex, buffer.readu16(EncodeA, KeyA * 2))
		local Sextet2 = (Byte1 % 16) * 4
		buffer.writeu8(Output, OutputIndex + 2, buffer.readu8(AlphabetBytes, Sextet2))
		buffer.writeu8(Output, OutputIndex + 3, PADDING_CHARACTER)
	end

	return Output
end

local function Decode(Input: buffer): buffer
	local InputLength = buffer.len(Input)
	if InputLength == 0 then
		return buffer.create(0)
	end

	local Padding = 0
	if buffer.readu8(Input, InputLength - 1) == PADDING_CHARACTER then
		Padding = 1
		if buffer.readu8(Input, InputLength - 2) == PADDING_CHARACTER then
			Padding = 2
		end
	end

	local Output = buffer.create((InputLength // 4) * 3 - Padding)
	local InputIndex = 0
	local OutputIndex = 0
	local BodyEnd = InputLength - 4
	local UnrollStop = BodyEnd - 8

	local DecodePair = DECODE_PAIR
	local Pair0 = PAIR_A_BYTE0
	local PairAHigh = PAIR_A_BYTE1_HIGH
	local PairBLow = PAIR_B_BYTE1_LOW
	local Pair2 = PAIR_B_BYTE2
	local Dtet = DECODE_TET

	while InputIndex <= UnrollStop do
		local Word0 = buffer.readu32(Input, InputIndex)
		local Word1 = buffer.readu32(Input, InputIndex + 4)

		local PairA0 = buffer.readu16(DecodePair, (Word0 % 65536) * 2)
		local PairB0 = buffer.readu16(DecodePair, (Word0 // 65536) * 2)
		buffer.writeu8(Output, OutputIndex, buffer.readu8(Pair0, PairA0))
		buffer.writeu8(Output, OutputIndex + 1, buffer.readu8(PairAHigh, PairA0) + buffer.readu8(PairBLow, PairB0))
		buffer.writeu8(Output, OutputIndex + 2, buffer.readu8(Pair2, PairB0))

		local PairA1 = buffer.readu16(DecodePair, (Word1 % 65536) * 2)
		local PairB1 = buffer.readu16(DecodePair, (Word1 // 65536) * 2)
		buffer.writeu8(Output, OutputIndex + 3, buffer.readu8(Pair0, PairA1))
		buffer.writeu8(Output, OutputIndex + 4, buffer.readu8(PairAHigh, PairA1) + buffer.readu8(PairBLow, PairB1))
		buffer.writeu8(Output, OutputIndex + 5, buffer.readu8(Pair2, PairB1))

		InputIndex += 8
		OutputIndex += 6
	end

	while InputIndex <= BodyEnd - 4 do
		local Word = buffer.readu32(Input, InputIndex)
		local PairA = buffer.readu16(DecodePair, (Word % 65536) * 2)
		local PairB = buffer.readu16(DecodePair, (Word // 65536) * 2)
		buffer.writeu8(Output, OutputIndex, buffer.readu8(Pair0, PairA))
		buffer.writeu8(Output, OutputIndex + 1, buffer.readu8(PairAHigh, PairA) + buffer.readu8(PairBLow, PairB))
		buffer.writeu8(Output, OutputIndex + 2, buffer.readu8(Pair2, PairB))
		InputIndex += 4
		OutputIndex += 3
	end

	local Char0 = buffer.readu8(Input, InputIndex)
	local Char1 = buffer.readu8(Input, InputIndex + 1)
	local Char2 = buffer.readu8(Input, InputIndex + 2)
	local Char3 = buffer.readu8(Input, InputIndex + 3)
	local Decoded0 = buffer.readu8(Dtet, Char0)
	local Decoded1 = buffer.readu8(Dtet, Char1)

	if Char2 == PADDING_CHARACTER then
		local PairA = Decoded0 * 64 + Decoded1
		buffer.writeu8(Output, OutputIndex, buffer.readu8(Pair0, PairA))
	elseif Char3 == PADDING_CHARACTER then
		local Decoded2 = buffer.readu8(Dtet, Char2)
		local PairA = Decoded0 * 64 + Decoded1
		local PairB = Decoded2 * 64
		buffer.writeu8(Output, OutputIndex, buffer.readu8(Pair0, PairA))
		buffer.writeu8(Output, OutputIndex + 1, buffer.readu8(PairAHigh, PairA) + (PairB // 256))
	else
		local Decoded2 = buffer.readu8(Dtet, Char2)
		local Decoded3 = buffer.readu8(Dtet, Char3)
		local PairA = Decoded0 * 64 + Decoded1
		local PairB = Decoded2 * 64 + Decoded3
		buffer.writeu8(Output, OutputIndex, buffer.readu8(Pair0, PairA))
		buffer.writeu8(Output, OutputIndex + 1, buffer.readu8(PairAHigh, PairA) + buffer.readu8(PairBLow, PairB))
		buffer.writeu8(Output, OutputIndex + 2, buffer.readu8(Pair2, PairB))
	end

	return Output
end

return {
	Encode = Encode,
	Decode = Decode
}


--[=[
	Cryptography library: Blake3

	Sizes:
		Key: 32 bytes
		Output: variable

	Return type: string (hex)
	Example usage:
		local Message = buffer.fromstring("Hello World")
		local Key = buffer.fromstring(string.rep("k", 32))

		--------Standard Hash--------
		local Hash = Blake3.Digest(Message, 32)

		--------Keyed Hash--------
		local KeyedHash = Blake3.DigestKeyed(Key, Message, 32)

		--------Key Derivation--------
		local Context = buffer.fromstring("my context")
		local KeyDeriver = Blake3.DeriveKey(Context)
		local DerivedKey = KeyDeriver(Message, 32)
--]=]

--!strict
--!optimize 2
--!native

local BLOCK_SIZE = 64
local CV_SIZE = 32
local EXTENDED_CV_SIZE = 64
local MAX_STACK_DEPTH = 64
local STACK_BUFFER_SIZE = MAX_STACK_DEPTH * CV_SIZE

local CHUNK_START = 0x01
local CHUNK_END = 0x02
local PARENT_FLAG = 0x04
local ROOT_FLAG = 0x08

local INITIAL_VECTORS = buffer.create(CV_SIZE) do
	local IV = {
		0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
		0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
	}
	for Index, Value in ipairs(IV) do
		buffer.writeu32(INITIAL_VECTORS, (Index - 1) * 4, Value)
	end
end

local function Compress(Hash: buffer, MessageBlock: buffer, Counter: number, V14: number, V15: number, IsFull: boolean?): buffer
	local Hash00 = buffer.readu32(Hash, 0)
	local Hash01 = buffer.readu32(Hash, 4)
	local Hash02 = buffer.readu32(Hash, 8)
	local Hash03 = buffer.readu32(Hash, 12)
	local Hash04 = buffer.readu32(Hash, 16)
	local Hash05 = buffer.readu32(Hash, 20)
	local Hash06 = buffer.readu32(Hash, 24)
	local Hash07 = buffer.readu32(Hash, 28)

	local V00, V01, V02, V03 = Hash00, Hash01, Hash02, Hash03
	local V04, V05, V06, V07 = Hash04, Hash05, Hash06, Hash07
	local V08, V09, V10, V11 = 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a

	local V12 = Counter % (2 ^ 32)
	local V13 = (Counter - V12) * (2 ^ -32)

	local M00 = buffer.readu32(MessageBlock, 0)
	local M01 = buffer.readu32(MessageBlock, 4)
	local M02 = buffer.readu32(MessageBlock, 8)
	local M03 = buffer.readu32(MessageBlock, 12)
	local M04 = buffer.readu32(MessageBlock, 16)
	local M05 = buffer.readu32(MessageBlock, 20)
	local M06 = buffer.readu32(MessageBlock, 24)
	local M07 = buffer.readu32(MessageBlock, 28)
	local M08 = buffer.readu32(MessageBlock, 32)
	local M09 = buffer.readu32(MessageBlock, 36)
	local M10 = buffer.readu32(MessageBlock, 40)
	local M11 = buffer.readu32(MessageBlock, 44)
	local M12 = buffer.readu32(MessageBlock, 48)
	local M13 = buffer.readu32(MessageBlock, 52)
	local M14 = buffer.readu32(MessageBlock, 56)
	local M15 = buffer.readu32(MessageBlock, 60)

	local XOR = bit32.bxor
	local LEFTROTATE = bit32.lrotate

	local Temp
	for Index = 1, 7 do
		V00 += V04 + M00; V12 = LEFTROTATE(XOR(V12, V00), 16)
		V08 += V12; V04 = LEFTROTATE(XOR(V04, V08), 20)
		V00 += V04 + M01; V12 = LEFTROTATE(XOR(V12, V00), 24)
		V08 += V12; V04 = LEFTROTATE(XOR(V04, V08), 25)

		V01 += V05 + M02; V13 = LEFTROTATE(XOR(V13, V01), 16)
		V09 += V13; V05 = LEFTROTATE(XOR(V05, V09), 20)
		V01 += V05 + M03; V13 = LEFTROTATE(XOR(V13, V01), 24)
		V09 += V13; V05 = LEFTROTATE(XOR(V05, V09), 25)

		V02 += V06 + M04; V14 = LEFTROTATE(XOR(V14, V02), 16)
		V10 += V14; V06 = LEFTROTATE(XOR(V06, V10), 20)
		V02 += V06 + M05; V14 = LEFTROTATE(XOR(V14, V02), 24)
		V10 += V14; V06 = LEFTROTATE(XOR(V06, V10), 25)

		V03 += V07 + M06; V15 = LEFTROTATE(XOR(V15, V03), 16)
		V11 += V15; V07 = LEFTROTATE(XOR(V07, V11), 20)
		V03 += V07 + M07; V15 = LEFTROTATE(XOR(V15, V03), 24)
		V11 += V15; V07 = LEFTROTATE(XOR(V07, V11), 25)

		V00 += V05 + M08; V15 = LEFTROTATE(XOR(V15, V00), 16)
		V10 += V15; V05 = LEFTROTATE(XOR(V05, V10), 20)
		V00 += V05 + M09; V15 = LEFTROTATE(XOR(V15, V00), 24)
		V10 += V15; V05 = LEFTROTATE(XOR(V05, V10), 25)

		V01 += V06 + M10; V12 = LEFTROTATE(XOR(V12, V01), 16)
		V11 += V12; V06 = LEFTROTATE(XOR(V06, V11), 20)
		V01 += V06 + M11; V12 = LEFTROTATE(XOR(V12, V01), 24)
		V11 += V12; V06 = LEFTROTATE(XOR(V06, V11), 25)

		V02 += V07 + M12; V13 = LEFTROTATE(XOR(V13, V02), 16)
		V08 += V13; V07 = LEFTROTATE(XOR(V07, V08), 20)
		V02 += V07 + M13; V13 = LEFTROTATE(XOR(V13, V02), 24)
		V08 += V13; V07 = LEFTROTATE(XOR(V07, V08), 25)

		V03 += V04 + M14; V14 = LEFTROTATE(XOR(V14, V03), 16)
		V09 += V14; V04 = LEFTROTATE(XOR(V04, V09), 20)
		V03 += V04 + M15; V14 = LEFTROTATE(XOR(V14, V03), 24)
		V09 += V14; V04 = LEFTROTATE(XOR(V04, V09), 25)

		if Index ~= 7 then
			Temp = M02
			M02 = M03
			M03 = M10
			M10 = M12
			M12 = M09
			M09 = M11
			M11 = M05
			M05 = M00
			M00 = Temp

			Temp = M06
			M06 = M04
			M04 = M07
			M07 = M13
			M13 = M14
			M14 = M15
			M15 = M08
			M08 = M01
			M01 = Temp
		end
	end

	if IsFull then
		local Result = buffer.create(EXTENDED_CV_SIZE)
		buffer.writeu32(Result, 0, XOR(V00, V08))
		buffer.writeu32(Result, 4, XOR(V01, V09))
		buffer.writeu32(Result, 8, XOR(V02, V10))
		buffer.writeu32(Result, 12, XOR(V03, V11))
		buffer.writeu32(Result, 16, XOR(V04, V12))
		buffer.writeu32(Result, 20, XOR(V05, V13))
		buffer.writeu32(Result, 24, XOR(V06, V14))
		buffer.writeu32(Result, 28, XOR(V07, V15))

		buffer.writeu32(Result, 32, XOR(V08, Hash00))
		buffer.writeu32(Result, 36, XOR(V09, Hash01))
		buffer.writeu32(Result, 40, XOR(V10, Hash02))
		buffer.writeu32(Result, 44, XOR(V11, Hash03))
		buffer.writeu32(Result, 48, XOR(V12, Hash04))
		buffer.writeu32(Result, 52, XOR(V13, Hash05))
		buffer.writeu32(Result, 56, XOR(V14, Hash06))
		buffer.writeu32(Result, 60, XOR(V15, Hash07))

		return Result
	else
		local Result = buffer.create(CV_SIZE)
		buffer.writeu32(Result, 0, XOR(V00, V08))
		buffer.writeu32(Result, 4, XOR(V01, V09))
		buffer.writeu32(Result, 8, XOR(V02, V10))
		buffer.writeu32(Result, 12, XOR(V03, V11))
		buffer.writeu32(Result, 16, XOR(V04, V12))
		buffer.writeu32(Result, 20, XOR(V05, V13))
		buffer.writeu32(Result, 24, XOR(V06, V14))
		buffer.writeu32(Result, 28, XOR(V07, V15))

		return Result
	end
end

local function ProcessMessage(InitialHashVector: buffer, Flags: number, Message: buffer, Length: number): buffer
	local MessageLength = buffer.len(Message)
	local StateCvs = buffer.create(STACK_BUFFER_SIZE)
	local StackSize = 0
	local StateCv = buffer.create(CV_SIZE)
	buffer.copy(StateCv, 0, InitialHashVector, 0, CV_SIZE)

	local StateCounter = 0
	local StateChunkNumber = 0
	local StateEndFlag = 0
	local StateStartFlag = CHUNK_START

	local BlockBuffer = buffer.create(BLOCK_SIZE)

	for BlockOffset = 0, MessageLength - BLOCK_SIZE - 1, BLOCK_SIZE do
		buffer.copy(BlockBuffer, 0, Message, BlockOffset, BLOCK_SIZE)
		local StateFlags = Flags + StateStartFlag + StateEndFlag

		StateCv = Compress(StateCv, BlockBuffer, StateCounter, BLOCK_SIZE, StateFlags)
		StateStartFlag = 0
		StateChunkNumber += 1

		if StateChunkNumber == 15 then
			StateEndFlag = CHUNK_END
		elseif StateChunkNumber == 16 then
			local MergeCv = StateCv
			local MergeAmount = StateCounter + 1

			while MergeAmount % 2 == 0 do
				StackSize = StackSize - 1
				local PopCv = buffer.create(CV_SIZE)
				buffer.copy(PopCv, 0, StateCvs, StackSize * CV_SIZE, CV_SIZE)

				local Block = buffer.create(EXTENDED_CV_SIZE)
				buffer.copy(Block, 0, PopCv, 0, CV_SIZE)
				buffer.copy(Block, CV_SIZE, MergeCv, 0, CV_SIZE)

				MergeCv = Compress(InitialHashVector, Block, 0, BLOCK_SIZE, Flags + PARENT_FLAG)
				MergeAmount = MergeAmount / 2
			end

			buffer.copy(StateCvs, StackSize * CV_SIZE, MergeCv, 0, CV_SIZE)
			StackSize = StackSize + 1
			buffer.copy(StateCv, 0, InitialHashVector, 0, CV_SIZE)
			StateStartFlag = CHUNK_START

			StateCounter += 1
			StateChunkNumber = 0
			StateEndFlag = 0
		end
	end

	local LastLength = MessageLength == 0 and 0 or ((MessageLength - 1) % BLOCK_SIZE + 1)
	local PaddedMessage = buffer.create(BLOCK_SIZE)

	if LastLength > 0 then
		buffer.copy(PaddedMessage, 0, Message, MessageLength - LastLength, LastLength)
	end

	local OutputCv: buffer
	local OutputBlock: buffer
	local OutputLength: number
	local OutputFlags: number

	if StateCounter > 0 then
		local StateFlags = Flags + StateStartFlag + CHUNK_END
		local MergeCv = Compress(StateCv, PaddedMessage, StateCounter, LastLength, StateFlags)

		for Index = StackSize, 2, -1 do
			local StackCv = buffer.create(CV_SIZE)
			buffer.copy(StackCv, 0, StateCvs, (Index - 1) * CV_SIZE, CV_SIZE)

			local Block = buffer.create(EXTENDED_CV_SIZE)
			buffer.copy(Block, 0, StackCv, 0, CV_SIZE)
			buffer.copy(Block, CV_SIZE, MergeCv, 0, CV_SIZE)

			MergeCv = Compress(InitialHashVector, Block, 0, BLOCK_SIZE, Flags + PARENT_FLAG)
		end

		OutputCv = InitialHashVector
		local FirstStackCv = buffer.create(CV_SIZE)
		buffer.copy(FirstStackCv, 0, StateCvs, 0, CV_SIZE)

		OutputBlock = buffer.create(EXTENDED_CV_SIZE)
		buffer.copy(OutputBlock, 0, FirstStackCv, 0, CV_SIZE)
		buffer.copy(OutputBlock, CV_SIZE, MergeCv, 0, CV_SIZE)

		OutputLength = BLOCK_SIZE
		OutputFlags = Flags + ROOT_FLAG + PARENT_FLAG
	else
		OutputCv = StateCv
		OutputBlock = PaddedMessage
		OutputLength = LastLength
		OutputFlags = Flags + StateStartFlag + CHUNK_END + ROOT_FLAG
	end

	local Output = buffer.create(Length)
	local OutputOffset = 0

	for Index = 0, Length // BLOCK_SIZE do
		local MessageDigest = Compress(OutputCv, OutputBlock, Index, OutputLength, OutputFlags, true)

		local BytesToCopy = math.min(BLOCK_SIZE, Length - OutputOffset)
		buffer.copy(Output, OutputOffset, MessageDigest, 0, BytesToCopy)
		OutputOffset += BytesToCopy

		if OutputOffset >= Length then
			break
		end
	end

	return Output
end

return function(Message: buffer, Length: number?): buffer
	return ProcessMessage(INITIAL_VECTORS, 0, Message, Length or 32)
end


--[=[
	Cryptography library: ChaCha20

	Sizes:
		Nonce: 12 bytes
		Key: 16/32 bytes

	Return type: buffer
	Example usage:
		local Data = buffer.fromstring("Hello World")
		local Key = buffer.fromstring(string.rep("k", 32))
		local Nonce = buffer.fromstring(string.rep("n", 12))

		--------Usage Case 1--------
		local Encrypted = ChaCha20(Data, Key, Nonce)

		--------Usage Case 2--------
		local Decrypted = ChaCha20(Encrypted, Key, Nonce)
--]=]

--!strict
--!native
--!optimize 2

local DWORD = 4
local BLOCK_SIZE = 64
local STATE_SIZE = 16

local CHACHA20_NONCE_SIZE = 12
local CHACHA20_KEY_SIZE_16 = 16
local CHACHA20_KEY_SIZE_32 = 32

local SIGMA_CONSTANTS = buffer.create(16) do
	local SigmaBytes = { string.byte("expand 32-byte k", 1, -1) }
	for Index, ByteValue in SigmaBytes do
		buffer.writeu8(SIGMA_CONSTANTS, Index - 1, ByteValue)
	end
end

local TAU_CONSTANTS = buffer.create(16) do
	local TauBytes = { string.byte("expand 16-byte k", 1, -1) }
	for Index, ByteValue in TauBytes do
		buffer.writeu8(TAU_CONSTANTS, Index - 1, ByteValue)
	end
end

local function ProcessBlock(InitialState: buffer, Rounds: number)
	local S0: number, S1: number, S2: number, S3: number, S4: number, S5: number, S6: number, S7: number, S8: number, S9: number, S10: number, S11: number, S12: number, S13: number, S14: number, S15: number =
		buffer.readu32(InitialState, 0), buffer.readu32(InitialState, 4),
		buffer.readu32(InitialState, 8), buffer.readu32(InitialState, 12),
		buffer.readu32(InitialState, 16), buffer.readu32(InitialState, 20),
		buffer.readu32(InitialState, 24), buffer.readu32(InitialState, 28),
		buffer.readu32(InitialState, 32), buffer.readu32(InitialState, 36),
		buffer.readu32(InitialState, 40), buffer.readu32(InitialState, 44),
		buffer.readu32(InitialState, 48), buffer.readu32(InitialState, 52),
		buffer.readu32(InitialState, 56), buffer.readu32(InitialState, 60)

	for Round = 1, Rounds, 2 do
		S0 = bit32.bor(S0 + S4, 0); S12 = bit32.lrotate(bit32.bxor(S12, S0), 16)
		S8 = bit32.bor(S8 + S12, 0); S4 = bit32.lrotate(bit32.bxor(S4, S8), 12)
		S0 = bit32.bor(S0 + S4, 0); S12 = bit32.lrotate(bit32.bxor(S12, S0), 8)
		S8 = bit32.bor(S8 + S12, 0); S4 = bit32.lrotate(bit32.bxor(S4, S8), 7)

		S1 = bit32.bor(S1 + S5, 0); S13 = bit32.lrotate(bit32.bxor(S13, S1), 16)
		S9 = bit32.bor(S9 + S13, 0); S5 = bit32.lrotate(bit32.bxor(S5, S9), 12)
		S1 = bit32.bor(S1 + S5, 0); S13 = bit32.lrotate(bit32.bxor(S13, S1), 8)
		S9 = bit32.bor(S9 + S13, 0); S5 = bit32.lrotate(bit32.bxor(S5, S9), 7)

		S2 = bit32.bor(S2 + S6, 0); S14 = bit32.lrotate(bit32.bxor(S14, S2), 16)
		S10 = bit32.bor(S10 + S14, 0); S6 = bit32.lrotate(bit32.bxor(S6, S10), 12)
		S2 = bit32.bor(S2 + S6, 0); S14 = bit32.lrotate(bit32.bxor(S14, S2), 8)
		S10 = bit32.bor(S10 + S14, 0); S6 = bit32.lrotate(bit32.bxor(S6, S10), 7)

		S3 = bit32.bor(S3 + S7, 0); S15 = bit32.lrotate(bit32.bxor(S15, S3), 16)
		S11 = bit32.bor(S11 + S15, 0); S7 = bit32.lrotate(bit32.bxor(S7, S11), 12)
		S3 = bit32.bor(S3 + S7, 0); S15 = bit32.lrotate(bit32.bxor(S15, S3), 8)
		S11 = bit32.bor(S11 + S15, 0); S7 = bit32.lrotate(bit32.bxor(S7, S11), 7)

		S0 = bit32.bor(S0 + S5, 0); S15 = bit32.lrotate(bit32.bxor(S15, S0), 16)
		S10 = bit32.bor(S10 + S15, 0); S5 = bit32.lrotate(bit32.bxor(S5, S10), 12)
		S0 = bit32.bor(S0 + S5, 0); S15 = bit32.lrotate(bit32.bxor(S15, S0), 8)
		S10 = bit32.bor(S10 + S15, 0); S5 = bit32.lrotate(bit32.bxor(S5, S10), 7)

		S1 = bit32.bor(S1 + S6, 0); S12 = bit32.lrotate(bit32.bxor(S12, S1), 16)
		S11 = bit32.bor(S11 + S12, 0); S6 = bit32.lrotate(bit32.bxor(S6, S11), 12)
		S1 = bit32.bor(S1 + S6, 0); S12 = bit32.lrotate(bit32.bxor(S12, S1), 8)
		S11 = bit32.bor(S11 + S12, 0); S6 = bit32.lrotate(bit32.bxor(S6, S11), 7)

		S2 = bit32.bor(S2 + S7, 0); S13 = bit32.lrotate(bit32.bxor(S13, S2), 16)
		S8 = bit32.bor(S8 + S13, 0); S7 = bit32.lrotate(bit32.bxor(S7, S8), 12)
		S2 = bit32.bor(S2 + S7, 0); S13 = bit32.lrotate(bit32.bxor(S13, S2), 8)
		S8 = bit32.bor(S8 + S13, 0); S7 = bit32.lrotate(bit32.bxor(S7, S8), 7)

		S3 = bit32.bor(S3 + S4, 0); S14 = bit32.lrotate(bit32.bxor(S14, S3), 16)
		S9 = bit32.bor(S9 + S14, 0); S4 = bit32.lrotate(bit32.bxor(S4, S9), 12)
		S3 = bit32.bor(S3 + S4, 0); S14 = bit32.lrotate(bit32.bxor(S14, S3), 8)
		S9 = bit32.bor(S9 + S14, 0); S4 = bit32.lrotate(bit32.bxor(S4, S9), 7)
	end

	buffer.writeu32(InitialState, 0, buffer.readu32(InitialState, 0) + S0)
	buffer.writeu32(InitialState, 4, buffer.readu32(InitialState, 4) + S1)
	buffer.writeu32(InitialState, 8, buffer.readu32(InitialState, 8) + S2)
	buffer.writeu32(InitialState, 12, buffer.readu32(InitialState, 12) + S3)
	buffer.writeu32(InitialState, 16, buffer.readu32(InitialState, 16) + S4)
	buffer.writeu32(InitialState, 20, buffer.readu32(InitialState, 20) + S5)
	buffer.writeu32(InitialState, 24, buffer.readu32(InitialState, 24) + S6)
	buffer.writeu32(InitialState, 28, buffer.readu32(InitialState, 28) + S7)
	buffer.writeu32(InitialState, 32, buffer.readu32(InitialState, 32) + S8)
	buffer.writeu32(InitialState, 36, buffer.readu32(InitialState, 36) + S9)
	buffer.writeu32(InitialState, 40, buffer.readu32(InitialState, 40) + S10)
	buffer.writeu32(InitialState, 44, buffer.readu32(InitialState, 44) + S11)
	buffer.writeu32(InitialState, 48, buffer.readu32(InitialState, 48) + S12)
	buffer.writeu32(InitialState, 52, buffer.readu32(InitialState, 52) + S13)
	buffer.writeu32(InitialState, 56, buffer.readu32(InitialState, 56) + S14)
	buffer.writeu32(InitialState, 60, buffer.readu32(InitialState, 60) + S15)
end

local function InitializeState(Key: buffer, Nonce: buffer, Counter: number): buffer
	local KeyLength = buffer.len(Key)
	local State = buffer.create(STATE_SIZE * DWORD)

	local Constants = KeyLength == 32 and SIGMA_CONSTANTS or TAU_CONSTANTS

	buffer.copy(State, 0, Constants, 0, 16)

	buffer.copy(State, 16, Key, 0, math.min(KeyLength, 16))
	if KeyLength == 32 then
		buffer.copy(State, 32, Key, 16, 16)
	else
		buffer.copy(State, 32, Key, 0, 16)
	end

	buffer.writeu32(State, 48, Counter)
	buffer.copy(State, 52, Nonce, 0, 12)

	return State
end

local function ChaCha20(Data: buffer, Key: buffer, Nonce: buffer, Counter: number?, Rounds: number?): buffer
	if Data == nil then
		error("Data cannot be nil", 2)
	end

	if typeof(Data) ~= "buffer" then
		error(`Data must be a buffer, got {typeof(Data)}`, 2)
	end

	if Key == nil then
		error("Key cannot be nil", 2)
	end

	if typeof(Key) ~= "buffer" then
		error(`Key must be a buffer, got {typeof(Key)}`, 2)
	end

	local KeyLength = buffer.len(Key)
	if KeyLength ~= CHACHA20_KEY_SIZE_16 and KeyLength ~= CHACHA20_KEY_SIZE_32 then
		error(`Key must be {CHACHA20_KEY_SIZE_16} or {CHACHA20_KEY_SIZE_32} bytes long, got {KeyLength} bytes`, 2)
	end

	if Nonce == nil then
		error("Nonce cannot be nil", 2)
	end

	if typeof(Nonce) ~= "buffer" then
		error(`Nonce must be a buffer, got {typeof(Nonce)}`, 2)
	end

	local NonceLength = buffer.len(Nonce)
	if NonceLength ~= CHACHA20_NONCE_SIZE then
		error(`Nonce must be exactly {CHACHA20_NONCE_SIZE} bytes long, got {NonceLength} bytes`, 2)
	end

	if Counter then
		if typeof(Counter) ~= "number" then
			error(`Counter must be a number, got {typeof(Counter)}`, 2)
		end

		if Counter < 0 then
			error(`Counter cannot be negative, got {Counter}`, 2)
		end

		if Counter ~= math.floor(Counter) then
			error(`Counter must be an integer, got {Counter}`, 2)
		end

		if Counter >= 2^32 then
			error(`Counter must be less than 2^32, got {Counter}`, 2)
		end
	end

	if Rounds then
		if typeof(Rounds) ~= "number" then
			error(`Rounds must be a number, got {typeof(Rounds)}`, 2)
		end

		if Rounds <= 0 then
			error(`Rounds must be positive, got {Rounds}`, 2)
		end

		if Rounds ~= math.floor(Rounds) then
			error(`Rounds must be an integer, got {Rounds}`, 2)
		end

		if Rounds % 2 ~= 0 then
			error(`Rounds must be even, got {Rounds}`, 2)
		end
	end

	local BlockCounter = Counter or 1
	local BlockRounds = Rounds or 20

	local DataLength = buffer.len(Data)
	if DataLength == 0 then
		return buffer.create(0)
	end

	local Output = buffer.create(DataLength)

	local DataOffset = 0

	local State = InitializeState(Key, Nonce, BlockCounter)
	local StateBackup = buffer.create(64)
	buffer.copy(StateBackup, 0, State, 0)

	while DataOffset < DataLength do
		ProcessBlock(State, BlockRounds)

		local BytesToProcess = math.min(BLOCK_SIZE, DataLength - DataOffset)
		local Leftover = BytesToProcess % 4

		for Index = 0, BytesToProcess - Leftover - 1, 4 do
			local DataWord = buffer.readu32(Data, DataOffset + Index)
			local KeystreamWord = buffer.readu32(State, Index)
			buffer.writeu32(Output, DataOffset + Index, bit32.bxor(DataWord, KeystreamWord))
		end

		for Index = BytesToProcess - Leftover, BytesToProcess - 1 do
			local DataByte = buffer.readu8(Data, DataOffset + Index)
			local KeystreamByte = buffer.readu8(State, Index)
			buffer.writeu8(Output, DataOffset + Index, bit32.bxor(DataByte, KeystreamByte))
		end

		DataOffset += BytesToProcess
		BlockCounter += 1
		buffer.copy(State, 0, StateBackup, 0)
		buffer.writeu32(State, 48, BlockCounter)
	end

	return Output
end

return ChaCha20


--[=[
	Cryptography library: Conversions

	Return type: string / buffer
	Example Usage:
		local HexString = Conversions.ToHex(buffer.fromstring("Hello World"))
		local OriginalBuffer = Conversions.FromHex("48656c6c6f20576f726c64")
--]=]

--!strict
--!optimize 2
--!native

local ENCODE_LOOKUP = buffer.create(256 * 2) do
	local HexChars = "0123456789abcdef"
	for Byte = 0, 255 do
		local HighNibble = bit32.rshift(Byte, 4)
		local LowNibble = Byte % 16

		local HighChar = string.byte(HexChars, HighNibble + 1)
		local LowChar = string.byte(HexChars, LowNibble + 1)

		local Combined = HighChar + bit32.lshift(LowChar, 8)
		buffer.writeu16(ENCODE_LOOKUP, Byte * 2, Combined)
	end
end

local DECODE_LOOKUP = buffer.create(65536 * 2) do
	for High = 0, 255 do
		for Low = 0, 255 do
			local HighNibble = 0
			local LowNibble = 0

			if High >= 48 and High <= 57 then
				HighNibble = High - 48
			elseif High >= 65 and High <= 70 then
				HighNibble = High - 55
			elseif High >= 97 and High <= 102 then
				HighNibble = High - 87
			else
				HighNibble = 0
			end

			if Low >= 48 and Low <= 57 then
				LowNibble = Low - 48
			elseif Low >= 65 and Low <= 70 then
				LowNibble = Low - 55
			elseif Low >= 97 and Low <= 102 then
				LowNibble = Low - 87
			else
				LowNibble = 0
			end

			local Combined = bit32.lshift(HighNibble, 4) + LowNibble
			local Index = bit32.lshift(Low, 8) + High
			buffer.writeu16(DECODE_LOOKUP, Index * 2, Combined)
		end
	end
end

local Conversions = {}

function Conversions.ToHex(Buffer: buffer): string
	local Length = buffer.len(Buffer)
	local Hex = buffer.create(Length * 2)

	local Lookup = ENCODE_LOOKUP

	local Leftover = Length % 8
	local HexCursor = 0

	for Index = 0, Length - Leftover - 1, 8 do
		local Hex1 = buffer.readu16(Lookup, buffer.readu8(Buffer, Index) * 2)
		local Hex2 = buffer.readu16(Lookup, buffer.readu8(Buffer, Index + 1) * 2)
		local Hex3 = buffer.readu16(Lookup, buffer.readu8(Buffer, Index + 2) * 2)
		local Hex4 = buffer.readu16(Lookup, buffer.readu8(Buffer, Index + 3) * 2)
		local Hex5 = buffer.readu16(Lookup, buffer.readu8(Buffer, Index + 4) * 2)
		local Hex6 = buffer.readu16(Lookup, buffer.readu8(Buffer, Index + 5) * 2)
		local Hex7 = buffer.readu16(Lookup, buffer.readu8(Buffer, Index + 6) * 2)
		local Hex8 = buffer.readu16(Lookup, buffer.readu8(Buffer, Index + 7) * 2)

		buffer.writeu16(Hex, HexCursor, Hex1)
		buffer.writeu16(Hex, HexCursor + 2, Hex2)
		buffer.writeu16(Hex, HexCursor + 4, Hex3)
		buffer.writeu16(Hex, HexCursor + 6, Hex4)
		buffer.writeu16(Hex, HexCursor + 8, Hex5)
		buffer.writeu16(Hex, HexCursor + 10, Hex6)
		buffer.writeu16(Hex, HexCursor + 12, Hex7)
		buffer.writeu16(Hex, HexCursor + 14, Hex8)

		HexCursor += 16
	end

	for Index = Length - Leftover, Length - 1 do
		local HexPair = buffer.readu16(Lookup, buffer.readu8(Buffer, Index) * 2)
		buffer.writeu16(Hex, HexCursor, HexPair)
		HexCursor += 2
	end

	return buffer.tostring(Hex)
end

function Conversions.FromHex(Hex: string | buffer): buffer
	local BufferHex = if type(Hex) == "string" then buffer.fromstring(Hex) else Hex
	local Length = buffer.len(BufferHex)
	if Length % 2 ~= 0 then
		error(`Length must be even, got {Length}`)
	end

	local Buffer = buffer.create(bit32.rshift(Length, 1))
	local Leftover = Length % 16
	local BufferCursor = 0
	local Lookup = DECODE_LOOKUP

	for Index = 0, Length - Leftover - 1, 16 do
		local Pair1 = buffer.readu16(BufferHex, Index)
		local Pair2 = buffer.readu16(BufferHex, Index + 2)
		local Pair3 = buffer.readu16(BufferHex, Index + 4)
		local Pair4 = buffer.readu16(BufferHex, Index + 6)
		local Pair5 = buffer.readu16(BufferHex, Index + 8)
		local Pair6 = buffer.readu16(BufferHex, Index + 10)
		local Pair7 = buffer.readu16(BufferHex, Index + 12)
		local Pair8 = buffer.readu16(BufferHex, Index + 14)

		local Byte1 = buffer.readu16(Lookup, Pair1 * 2)
		local Byte2 = buffer.readu16(Lookup, Pair2 * 2)
		local Byte3 = buffer.readu16(Lookup, Pair3 * 2)
		local Byte4 = buffer.readu16(Lookup, Pair4 * 2)
		local Byte5 = buffer.readu16(Lookup, Pair5 * 2)
		local Byte6 = buffer.readu16(Lookup, Pair6 * 2)
		local Byte7 = buffer.readu16(Lookup, Pair7 * 2)
		local Byte8 = buffer.readu16(Lookup, Pair8 * 2)

		local Chunk1 = bit32.lshift(Byte4, 24) + bit32.lshift(Byte3, 16) +
			bit32.lshift(Byte2, 8) + Byte1
		local Chunk2 = bit32.lshift(Byte8, 24) + bit32.lshift(Byte7, 16) +
			bit32.lshift(Byte6, 8) + Byte5

		buffer.writeu32(Buffer, BufferCursor, Chunk1)
		buffer.writeu32(Buffer, BufferCursor + 4, Chunk2)
		BufferCursor += 8
	end

	for Index = Length - Leftover, Length - 1, 2 do
		local Pair = buffer.readu16(BufferHex, Index)
		local Byte = buffer.readu16(Lookup, Pair * 2)
		buffer.writeu8(Buffer, BufferCursor, Byte)
		BufferCursor += 1
	end

	return Buffer
end

return Conversions


--[=[
	Cryptography library: Cryptographically Secure RNG

	Usage:
		local RandomFloat = CSPRNG.Random()
		local RandomInt = CSPRNG.RandomInt(1, 100)
		local RandomNumber = CSPRNG.RandomNumber(0.5, 10.5)
		local RandomBytes = CSPRNG.RandomBytes(32)
		local RandomHex = CSPRNG.RandomHex(16)
		local FastString = CSPRNG.RandomString(16, false)
		local FastBuffer = CSPRNG.RandomString(32, true)

		local Ed25519Clamped = CSPRNG.Ed25519ClampedBytes(SomeBuffer)
		local Ed25519Random = CSPRNG.Ed25519Random()

		CSPRNG.AddEntropyProvider(function)
		CSPRNG.RemoveEntropyProvider(function)
		CSPRNG.Reseed()

		CSPRNG.BytesLeft
--]=]

--!strict
--!optimize 2
--!native

local Conversions = require("@self/Conversions")
local ChaCha20 = require("@self/ChaCha20")
local Blake3 = require("@self/Blake3")

export type EntropyProvider = (BytesLeft: number) -> buffer?

type CSPRNGModule = {
	BlockExpansion: boolean,

	SizeTarget: number,
	RekeyAfter: number,

	Key: buffer,
	Nonce: buffer,
	Buffer: buffer,

	Counter: number,
	BufferPosition: number,
	BufferSize: number,
	BytesLeft: number,

	EntropyProviders: { EntropyProvider },

	Reseed: (CustomEntropy: buffer?) -> (),
	AddEntropyProvider: (ProviderFunction: EntropyProvider) -> (),
	RemoveEntropyProvider: (ProviderFunction: EntropyProvider) -> (),

	Random: () -> number,
	RandomInt: (Min: number, Max: number?) -> number,
	RandomNumber: (Min: number, Max: number?) -> number,
	RandomBytes: (Count: number) -> buffer,
	RandomString: (Length: number, AsBuffer: boolean?) -> string | buffer,
	RandomHex: (Length: number) -> string,
	Ed25519ClampedBytes: (Input: buffer) -> buffer,
	Ed25519Random: () -> buffer,
}

local BLOCK_SIZE = 64
local KEY_SIZE = 32
local NONCE_SIZE = 12

local CSPRNG: CSPRNGModule = {
	BlockExpansion = true,
	SizeTarget = 2048,
	RekeyAfter = 1024,

	Key = buffer.create(0),
	Nonce = buffer.create(0),
	Buffer = buffer.create(0),

	Counter = 0,
	BufferPosition = 0,
	BufferSize = 0,
	BytesLeft = 0,

	EntropyProviders = {}
} :: CSPRNGModule

local INPUT_BUFFER = buffer.create(BLOCK_SIZE)

local function Reset()
	CSPRNG.Key = buffer.create(0)
	CSPRNG.Nonce = buffer.create(0)
	CSPRNG.Buffer = buffer.create(0)

	CSPRNG.Counter = 0
	CSPRNG.BufferPosition = 0
	CSPRNG.BufferSize = 0
end

local function GatherEntropy(CustomEntropy: buffer?): number
	local EntropyBuffers = buffer.create(1024)
	local Offset = 0

	local function WriteToBuffer(Source: buffer)
		local Size = buffer.len(Source)
		buffer.copy(EntropyBuffers, Offset, Source, 0, Size)
		Offset += Size
	end

	local CurrentTime = 1.234
	if tick then
		CurrentTime = tick()
		local TimeBuffer = buffer.create(8)
		buffer.writef64(TimeBuffer, 0, CurrentTime)
		WriteToBuffer(TimeBuffer)
	end

	local ClockTime = os.clock()
	local ClockBuffer = buffer.create(8)
	buffer.writef64(ClockBuffer, 0, ClockTime)
	WriteToBuffer(ClockBuffer)

	local UnixTime = os.time()
	local UnixBuffer = buffer.create(8)
	buffer.writeu32(UnixBuffer, 0, UnixTime % 0x100000000)
	buffer.writeu32(UnixBuffer, 4, math.floor(UnixTime / 0x100000000))
	WriteToBuffer(UnixBuffer)

	local DateTimeMillis = 5.678
	if DateTime then
		DateTimeMillis = DateTime.now().UnixTimestampMillis
		local DateTimeBuffer = buffer.create(8)
		buffer.writef64(DateTimeBuffer, 0, DateTimeMillis)
		WriteToBuffer(DateTimeBuffer)

		local DateTimePrecisionBuffer = buffer.create(16)
		buffer.writef32(DateTimePrecisionBuffer, 0, DateTimeMillis / 1000)
		buffer.writef32(DateTimePrecisionBuffer, 4, (DateTimeMillis % 1000) / 100)
		buffer.writef32(DateTimePrecisionBuffer, 8, DateTimeMillis / 86400000)
		buffer.writef32(DateTimePrecisionBuffer, 12, (DateTimeMillis * 0.001) % 1)
		WriteToBuffer(DateTimePrecisionBuffer)
	else
		WriteToBuffer(buffer.create(24))
	end

	local FracTimeBuffer = buffer.create(16)
	buffer.writef32(FracTimeBuffer, 0, ClockTime / 100)
	buffer.writef32(FracTimeBuffer, 4, CurrentTime / 1000)
	buffer.writef32(FracTimeBuffer, 8, (ClockTime * 12345.6789) % 1)
	buffer.writef32(FracTimeBuffer, 12, (CurrentTime * 98765.4321) % 1)
	WriteToBuffer(FracTimeBuffer)

	local NoiseBuffer = buffer.create(32)
	for Index = 0, 7 do
		local Noise1 = math.noise(ClockTime + Index, UnixTime + Index, ClockTime + UnixTime + Index)
		local Noise2 = math.noise(CurrentTime + Index * 0.1, DateTimeMillis * 0.0001 + Index, ClockTime * 1.5 + Index)
		local Noise3 = math.noise(UnixTime * 0.01 + Index, ClockTime + DateTimeMillis * 0.001, CurrentTime + Index * 2)
		local Noise4 = math.noise(DateTimeMillis * 0.00001 + Index, UnixTime + ClockTime + Index, CurrentTime * 0.1 + Index)

		buffer.writef32(NoiseBuffer, Index * 4, Noise1 + Noise2 + Noise3 + Noise4)
	end
	WriteToBuffer(NoiseBuffer)

	local BenchmarkTimings = buffer.create(32)
	for Index = 0, 7 do
		local StartTime = os.clock()
		local Sum = 0

		local Iterations = 50 + (Index * 25)
		for Iteration = 1, Iterations do
			Sum += Iteration * Iteration + math.sin(Iteration / 10) * math.cos(Iteration / 7)
		end

		local EndTime = os.clock()
		local TimingDelta = EndTime - StartTime
		buffer.writef32(BenchmarkTimings, Index * 4, TimingDelta * 1000000)
	end
	WriteToBuffer(BenchmarkTimings)

	local AllocTimings = buffer.create(24)
	for Index = 0, 5 do
		local AllocStart = os.clock()

		for AllocIndex = 1, 20 do
			local _TempBuf = buffer.create(64 + AllocIndex)
		end

		local AllocEnd = os.clock()
		buffer.writef32(AllocTimings, Index * 4, (AllocEnd - AllocStart) * 10000000)
	end
	WriteToBuffer(AllocTimings)

	local MicroTime = math.floor(CurrentTime * 1000000)
	local MicroTimeBuffer = buffer.create(8)
	buffer.writeu32(MicroTimeBuffer, 0, MicroTime % 0x100000000)
	buffer.writeu32(MicroTimeBuffer, 4, math.floor(MicroTime / 0x100000000))
	WriteToBuffer(MicroTimeBuffer)

	if game then
		if game.JobId and #game.JobId > 0 then
			local JobIdBuffer = buffer.fromstring(game.JobId)
			WriteToBuffer(JobIdBuffer)
		end

		if game.PlaceId then
			local PlaceIdBuffer = buffer.create(8)
			buffer.writeu32(PlaceIdBuffer, 0, game.PlaceId % 0x100000000)
			buffer.writeu32(PlaceIdBuffer, 4, math.floor(game.PlaceId / 0x100000000))
			WriteToBuffer(PlaceIdBuffer)
		end

		if workspace and workspace.DistributedGameTime then
			local DistTimeBuffer = buffer.create(8)
			buffer.writef64(DistTimeBuffer, 0, workspace.DistributedGameTime)
			WriteToBuffer(DistTimeBuffer)

			local DistMicroTime = math.floor(workspace.DistributedGameTime * 1000000)
			local DistMicroBuffer = buffer.create(8)
			buffer.writeu32(DistMicroBuffer, 0, DistMicroTime % 0x100000000)
			buffer.writeu32(DistMicroBuffer, 4, math.floor(DistMicroTime / 0x100000000))
			WriteToBuffer(DistMicroBuffer)
		end
	end

	local AddressEntropy = buffer.create(128)
	for Index = 0, 7 do
		local TempTable = {}
		local TempFunc = function() end
		local TempBuffer = buffer.create(0)
		local TempUserdata = newproxy()

		local TableAddr = string.gsub(tostring(TempTable), "table: ", "")
		local FuncAddr = string.gsub(tostring(TempFunc), "function: ", "")
		local BufferAddr = string.gsub(tostring(TempBuffer), "buffer: ", "")
		local UserdataAddr = string.gsub(tostring(TempUserdata), "userdata: ", "")

		local TableHash = 0
		local ThreadHash = 0
		local FuncHash = 0
		local BufferHash = 0
		local UserdataHash = 0

		for AddrIndex = 1, #TableAddr do
			TableHash = bit32.bxor(TableHash, string.byte(TableAddr, AddrIndex)) * 31
		end

		if coroutine then
			local ThreadAddr = string.gsub(tostring(coroutine.create(function() end)), "thread: ", "")
			for AddrIndex = 1, #ThreadAddr do
				ThreadHash = bit32.bxor(ThreadHash, string.byte(ThreadAddr, AddrIndex)) * 31
			end
		end

		for AddrIndex = 1, #FuncAddr do
			FuncHash = bit32.bxor(FuncHash, string.byte(FuncAddr, AddrIndex)) * 37
		end
		for AddrIndex = 1, #BufferAddr do
			BufferHash = bit32.bxor(BufferHash, string.byte(BufferAddr, AddrIndex)) * 41
		end
		for AddrIndex = 1, #UserdataAddr do
			UserdataHash = bit32.bxor(UserdataHash, string.byte(UserdataAddr, AddrIndex)) * 43
		end

		buffer.writeu32(AddressEntropy, Index * 16, TableHash)
		buffer.writeu32(AddressEntropy, Index * 16 + 4, ThreadHash)
		buffer.writeu32(AddressEntropy, Index * 16 + 8, FuncHash)
		buffer.writeu32(AddressEntropy, Index * 16 + 12, bit32.bxor(BufferHash, UserdataHash))
	end
	WriteToBuffer(AddressEntropy)

	local function AddExtraEntropy(Entropy: buffer?, Warn: boolean, Provider: string?)
		if not Entropy then
			return
		end

		local BytesLeft = 1024 - Offset

		if BytesLeft > 0 then
			local Extra = buffer.len(Entropy) - BytesLeft
			local Truncated = math.min(BytesLeft, buffer.len(Entropy))

			if Extra > 0 and Warn and Provider then
				warn(`CSPRNG: {Provider} returned {Extra} bytes more than available and was truncated to {Truncated} bytes`)
			end

			buffer.copy(EntropyBuffers, Offset, Entropy, 0, Truncated)
		end
	end

	for Index, Provider in CSPRNG.EntropyProviders do
		local BytesLeft = 1024 - Offset
		if BytesLeft > 0 then
			local Success: boolean, ExtraEntropy: buffer? = pcall(Provider, BytesLeft)
			if not Success then
				warn(`CSPRNG Provider errored with {ExtraEntropy}`)
			end

			AddExtraEntropy(ExtraEntropy, true, `Entropy Provider #{Index}`)
		end
	end

	if CustomEntropy then
		AddExtraEntropy(CustomEntropy, false)
	end

	local KeyMaterial = Blake3(EntropyBuffers, KEY_SIZE + NONCE_SIZE)

	CSPRNG.Key = buffer.create(KEY_SIZE)
	buffer.copy(CSPRNG.Key, 0, KeyMaterial, 0, KEY_SIZE)

	CSPRNG.Nonce = buffer.create(NONCE_SIZE)
	buffer.copy(CSPRNG.Nonce, 0, KeyMaterial, KEY_SIZE, NONCE_SIZE)

	return buffer.len(EntropyBuffers) - Offset
end

local function GenerateBlock()
	buffer.fill(INPUT_BUFFER, 0, 0, BLOCK_SIZE)
	local ChaChaOutput = ChaCha20(INPUT_BUFFER, CSPRNG.Key, CSPRNG.Nonce, CSPRNG.Counter, 20)
	local RekeyThreshold = math.max(math.floor(CSPRNG.RekeyAfter), 2)
	local SizeTargetClamped = math.clamp(math.floor(CSPRNG.SizeTarget), 64, 4294967295)

	CSPRNG.Buffer = if CSPRNG.BlockExpansion then Blake3(ChaChaOutput, SizeTargetClamped) else ChaChaOutput
	CSPRNG.BufferPosition = 0
	CSPRNG.BufferSize = buffer.len(CSPRNG.Buffer)
	CSPRNG.Counter += 1

	if CSPRNG.Counter % RekeyThreshold == 0 then
		GatherEntropy()
		CSPRNG.Counter = 0
	end
end

local function GetBytes(Count: number): buffer
	local Result = buffer.create(Count)
	local ResultPosition = 0

	while ResultPosition < Count do
		if CSPRNG.BufferPosition >= CSPRNG.BufferSize then
			GenerateBlock()
		end

		local BytesNeeded = Count - ResultPosition
		local BytesAvailable = CSPRNG.BufferSize - CSPRNG.BufferPosition
		local BytesToCopy = math.min(BytesNeeded, BytesAvailable)

		buffer.copy(Result, ResultPosition, CSPRNG.Buffer, CSPRNG.BufferPosition, BytesToCopy)
		ResultPosition += BytesToCopy
		CSPRNG.BufferPosition += BytesToCopy
	end

	return Result
end

local function GetFloat(): number
	if CSPRNG.BufferPosition + 8 > CSPRNG.BufferSize then
		GenerateBlock()
	end

	local Value1 = buffer.readu32(CSPRNG.Buffer, CSPRNG.BufferPosition)
	local Value2 = buffer.readu32(CSPRNG.Buffer, CSPRNG.BufferPosition + 4)
	CSPRNG.BufferPosition += 8

	local High = bit32.rshift(Value1, 5)
	local Low = bit32.rshift(Value2, 6)

	return (High * 67108864.0 + Low) / 9007199254740992.0
end

local function GetIntRange(Min: number, Max: number): number
	local Range = Max - Min + 1
	local MaxUInt32 = 0xFFFFFFFF
	local Limit = MaxUInt32 - (MaxUInt32 % Range)

	if CSPRNG.BufferPosition + 4 > CSPRNG.BufferSize then
		GenerateBlock()
	end

	local Value = buffer.readu32(CSPRNG.Buffer, CSPRNG.BufferPosition)
	CSPRNG.BufferPosition += 4

	if bit32.band(Range, Range - 1) == 0 then
		return Min + bit32.band(Value, Range - 1)
	else
		while Value > Limit do
			if CSPRNG.BufferPosition + 4 > CSPRNG.BufferSize then
				GenerateBlock()
			end
			Value = buffer.readu32(CSPRNG.Buffer, CSPRNG.BufferPosition)
			CSPRNG.BufferPosition += 4
		end

		return Min + (Value % Range)
	end
end

local function GetNumberRange(Min: number, Max: number): number
	if Min > Max then
		Min, Max = Max, Min
	end

	local Range = Max - Min
	if Range <= 0 then
		return Min
	end

	return Min + (GetFloat() * Range)
end

local function GetRandomString(Length: number, AsBuffer: boolean?): string | buffer
	local Characters = buffer.create(Length)

	for Index = 0, Length - 1 do
		buffer.writeu8(Characters, Index, GetIntRange(36, 122))
	end

	return if AsBuffer
		then Characters
		else buffer.tostring(Characters)
end

local function GetEd25519RandomBytes(): buffer
	local Output = buffer.create(32)

	for Index = 0, 31 do
		buffer.writeu8(Output, Index, GetIntRange(0, 255))
	end

	return Output
end

local function GetEd25519ClampedBytes(Input: buffer): buffer
	local Output = buffer.create(32)
	buffer.copy(Output, 0, Input, 0, 32)

	local FirstByte = buffer.readu8(Output, 0)
	FirstByte = bit32.band(FirstByte, 0xF8)
	buffer.writeu8(Output, 0, FirstByte)

	local LastByte = buffer.readu8(Output, 31)
	LastByte = bit32.band(LastByte, 0x7F)
	LastByte = bit32.bor(LastByte, 0x40)
	buffer.writeu8(Output, 31, LastByte)

	local HasVariation = false
	local FirstMiddleByte = buffer.readu8(Output, 1)
	for Index = 2, 30 do
		if buffer.readu8(Output, Index) ~= FirstMiddleByte then
			HasVariation = true
			break
		end
	end

	if not HasVariation then
		buffer.writeu8(Output, 15, bit32.bxor(FirstMiddleByte, 0x55))
	end

	return Output
end

local function GetHexString(Length: number): string
	local BytesNeeded = Length / 2
	local Bytes = GetBytes(BytesNeeded)
	local Hex = Conversions.ToHex(Bytes)

	return Hex
end

function CSPRNG.AddEntropyProvider(ProviderFunction: EntropyProvider)
	table.insert(CSPRNG.EntropyProviders, ProviderFunction)
end

function CSPRNG.RemoveEntropyProvider(ProviderFunction: EntropyProvider)
	for Index = #CSPRNG.EntropyProviders, 1, -1 do
		if CSPRNG.EntropyProviders[Index] == ProviderFunction then
			table.remove(CSPRNG.EntropyProviders, Index)
			break
		end
	end
end

function CSPRNG.Random(): number
	return GetFloat()
end

function CSPRNG.RandomInt(Min: number, Max: number?): number
	if Max and type(Max) ~= "number" then
		error(`Max must be a number or nil, got {typeof(Max)}`, 2)
	end

	if type(Min) ~= "number" then
		error(`Min must be a number, got {typeof(Min)}`, 2)
	end

	if Max and Max < Min then
		error(`Max ({Max}) can't be less than Min ({Min})`, 2)
	end

	if Max and Max == Min then
		error(`Max ({Max}) can't be equal to Min ({Min})`, 2)
	end

	local ActualMax: number
	local ActualMin: number

	if Max == nil then
		ActualMax = Min
		ActualMin = 1
	else
		ActualMax = Max
		ActualMin = Min
	end

	return GetIntRange(ActualMin, ActualMax)
end

function CSPRNG.RandomNumber(Min: number, Max: number?): number
	if Max and type(Max) ~= "number" then
		error(`Max must be a number or nil, got {typeof(Max)}`, 2)
	end

	if type(Min) ~= "number" then
		error(`Min must be a number, got {typeof(Min)}`, 2)
	end

	if Max and Max < Min then
		error(`Max ({Max}) must be bigger than Min ({Min})`, 2)
	end

	if Max and Max == Min then
		error(`Max ({Max}) can't be equal to Min ({Min})`, 2)
	end

	local ActualMax: number
	local ActualMin: number

	if Max == nil then
		ActualMax = Min
		ActualMin = 0
	else
		ActualMax = Max
		ActualMin = Min
	end

	return GetNumberRange(ActualMin, ActualMax)
end

function CSPRNG.RandomBytes(Count: number): buffer
	if type(Count) ~= "number" then
		error(`Count must be a number, got {typeof(Count)}`, 2)
	end

	if Count <= 0 then
		error(`Count must be bigger than 0, got {Count}`, 2)
	end

	if Count % 1 ~= 0 then
		error("Count must be an integer", 2)
	end

	return GetBytes(Count)
end

function CSPRNG.RandomString(Length: number, AsBuffer: boolean?): string | buffer
	if type(Length) ~= "number" then
		error(`Length must be a number, got {typeof(Length)}`, 2)
	end

	if Length <= 0 then
		error(`Length must be bigger than 0, got {Length}`, 2)
	end

	if Length % 1 ~= 0 then
		error("Length must be an integer", 2)
	end

	if AsBuffer ~= nil and type(AsBuffer) ~= "boolean" then
		error(`AsBuffer must be a boolean or nil, got {typeof(AsBuffer)}`, 2)
	end

	return GetRandomString(Length, AsBuffer)
end

function CSPRNG.RandomHex(Length: number): string
	if type(Length) ~= "number" then
		error(`Length must be a number, got {typeof(Length)}`, 2)
	end

	if Length <= 0 then
		error(`Length must be bigger than 0, got {Length}`, 2)
	end

	if Length % 1 ~= 0 then
		error("Length must be an integer", 2)
	end

	if Length % 2 ~= 0 then
		error(`Length must be even, got {Length}`, 2)
	end

	return GetHexString(Length)
end

function CSPRNG.Ed25519ClampedBytes(Input: buffer): buffer
	if type(Input) ~= "buffer" then
		error(`Input must be a buffer, got {typeof(Input)}`, 2)
	end

	return GetEd25519ClampedBytes(Input)
end

function CSPRNG.Ed25519Random(): buffer
	return GetEd25519ClampedBytes(GetEd25519RandomBytes())
end

function CSPRNG.Reseed(CustomEntropy: buffer?)
	if CustomEntropy ~= nil and type(CustomEntropy) ~= "buffer" then
		error(`CustomEntropy must be a buffer or nil, got {typeof(CustomEntropy)}`, 2)
	end

	Reset()
	GatherEntropy(CustomEntropy)
end

CSPRNG.BytesLeft = GatherEntropy()
GenerateBlock()

return CSPRNG


--[=[
	Cryptography library: Random String Generator

	⚠WARNING: This is not using cryptographically secure random numbers.
	For Security use CSPRNG.⚠

	Return type: string | buffer
	Example Usage:
		local String = RandomString(500)
--]=]

--!strict
--!optimize 2
--!native

local function RandomString(Length: number, AsBuffer: boolean?): string | buffer
	local Characters = buffer.create(Length)

	for Index = 0, Length - 1 do
		buffer.writeu8(Characters, Index, math.random(36, 122))
	end

	return if AsBuffer
		then Characters
		else buffer.tostring(Characters)
end

return RandomString


--!strict

local Algorithms = table.freeze({
	RandomString = require("@self/RandomString"),
	Conversions = require("@self/Conversions"),
	Base64 = require("@self/Base64"),
	CSPRNG = require("@self/CSPRNG")
})

return Algorithms


--[=[
	Cryptography library: Blake3

	Sizes:
		Key: 32 bytes
		Output: variable

	Return type: string (hex)
	Example usage:
		local Message = buffer.fromstring("Hello World")
		local Key = buffer.fromstring(string.rep("k", 32))

		--------Standard Hash--------
		local Hash = Blake3.Digest(Message, 32)

		--------Keyed Hash--------
		local KeyedHash = Blake3.DigestKeyed(Key, Message, 32)

		--------Key Derivation--------
		local Context = buffer.fromstring("my context")
		local KeyDeriver = Blake3.DeriveKey(Context)
		local DerivedKey = KeyDeriver(Message, 32)
--]=]

--!strict
--!optimize 2
--!native

local BLOCK_SIZE = 64
local CV_SIZE = 32
local EXTENDED_CV_SIZE = 64
local MAX_STACK_DEPTH = 64
local STACK_BUFFER_SIZE = MAX_STACK_DEPTH * CV_SIZE

local CHUNK_START = 0x01
local CHUNK_END = 0x02
local PARENT_FLAG = 0x04
local ROOT_FLAG = 0x08

local INITIAL_VECTORS = buffer.create(CV_SIZE) do
	local IV = {
		0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
		0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
	}
	for Index, Value in ipairs(IV) do
		buffer.writeu32(INITIAL_VECTORS, (Index - 1) * 4, Value)
	end
end

local function Compress(Hash: buffer, MessageBlock: buffer, Counter: number, V14: number, V15: number, IsFull: boolean?): buffer
	local Hash00 = buffer.readu32(Hash, 0)
	local Hash01 = buffer.readu32(Hash, 4)
	local Hash02 = buffer.readu32(Hash, 8)
	local Hash03 = buffer.readu32(Hash, 12)
	local Hash04 = buffer.readu32(Hash, 16)
	local Hash05 = buffer.readu32(Hash, 20)
	local Hash06 = buffer.readu32(Hash, 24)
	local Hash07 = buffer.readu32(Hash, 28)

	local V00, V01, V02, V03 = Hash00, Hash01, Hash02, Hash03
	local V04, V05, V06, V07 = Hash04, Hash05, Hash06, Hash07
	local V08, V09, V10, V11 = 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a

	local V12 = Counter % (2 ^ 32)
	local V13 = (Counter - V12) * (2 ^ -32)

	local M00 = buffer.readu32(MessageBlock, 0)
	local M01 = buffer.readu32(MessageBlock, 4)
	local M02 = buffer.readu32(MessageBlock, 8)
	local M03 = buffer.readu32(MessageBlock, 12)
	local M04 = buffer.readu32(MessageBlock, 16)
	local M05 = buffer.readu32(MessageBlock, 20)
	local M06 = buffer.readu32(MessageBlock, 24)
	local M07 = buffer.readu32(MessageBlock, 28)
	local M08 = buffer.readu32(MessageBlock, 32)
	local M09 = buffer.readu32(MessageBlock, 36)
	local M10 = buffer.readu32(MessageBlock, 40)
	local M11 = buffer.readu32(MessageBlock, 44)
	local M12 = buffer.readu32(MessageBlock, 48)
	local M13 = buffer.readu32(MessageBlock, 52)
	local M14 = buffer.readu32(MessageBlock, 56)
	local M15 = buffer.readu32(MessageBlock, 60)

	local Temp
	for Index = 1, 7 do
		V00 += V04 + M00; V12 = bit32.lrotate(bit32.bxor(V12, V00), 16)
		V08 += V12; V04 = bit32.lrotate(bit32.bxor(V04, V08), 20)
		V00 += V04 + M01; V12 = bit32.lrotate(bit32.bxor(V12, V00), 24)
		V08 += V12; V04 = bit32.lrotate(bit32.bxor(V04, V08), 25)

		V01 += V05 + M02; V13 = bit32.lrotate(bit32.bxor(V13, V01), 16)
		V09 += V13; V05 = bit32.lrotate(bit32.bxor(V05, V09), 20)
		V01 += V05 + M03; V13 = bit32.lrotate(bit32.bxor(V13, V01), 24)
		V09 += V13; V05 = bit32.lrotate(bit32.bxor(V05, V09), 25)

		V02 += V06 + M04; V14 = bit32.lrotate(bit32.bxor(V14, V02), 16)
		V10 += V14; V06 = bit32.lrotate(bit32.bxor(V06, V10), 20)
		V02 += V06 + M05; V14 = bit32.lrotate(bit32.bxor(V14, V02), 24)
		V10 += V14; V06 = bit32.lrotate(bit32.bxor(V06, V10), 25)

		V03 += V07 + M06; V15 = bit32.lrotate(bit32.bxor(V15, V03), 16)
		V11 += V15; V07 = bit32.lrotate(bit32.bxor(V07, V11), 20)
		V03 += V07 + M07; V15 = bit32.lrotate(bit32.bxor(V15, V03), 24)
		V11 += V15; V07 = bit32.lrotate(bit32.bxor(V07, V11), 25)

		V00 += V05 + M08; V15 = bit32.lrotate(bit32.bxor(V15, V00), 16)
		V10 += V15; V05 = bit32.lrotate(bit32.bxor(V05, V10), 20)
		V00 += V05 + M09; V15 = bit32.lrotate(bit32.bxor(V15, V00), 24)
		V10 += V15; V05 = bit32.lrotate(bit32.bxor(V05, V10), 25)

		V01 += V06 + M10; V12 = bit32.lrotate(bit32.bxor(V12, V01), 16)
		V11 += V12; V06 = bit32.lrotate(bit32.bxor(V06, V11), 20)
		V01 += V06 + M11; V12 = bit32.lrotate(bit32.bxor(V12, V01), 24)
		V11 += V12; V06 = bit32.lrotate(bit32.bxor(V06, V11), 25)

		V02 += V07 + M12; V13 = bit32.lrotate(bit32.bxor(V13, V02), 16)
		V08 += V13; V07 = bit32.lrotate(bit32.bxor(V07, V08), 20)
		V02 += V07 + M13; V13 = bit32.lrotate(bit32.bxor(V13, V02), 24)
		V08 += V13; V07 = bit32.lrotate(bit32.bxor(V07, V08), 25)

		V03 += V04 + M14; V14 = bit32.lrotate(bit32.bxor(V14, V03), 16)
		V09 += V14; V04 = bit32.lrotate(bit32.bxor(V04, V09), 20)
		V03 += V04 + M15; V14 = bit32.lrotate(bit32.bxor(V14, V03), 24)
		V09 += V14; V04 = bit32.lrotate(bit32.bxor(V04, V09), 25)

		if Index ~= 7 then
			Temp = M02
			M02 = M03
			M03 = M10
			M10 = M12
			M12 = M09
			M09 = M11
			M11 = M05
			M05 = M00
			M00 = Temp

			Temp = M06
			M06 = M04
			M04 = M07
			M07 = M13
			M13 = M14
			M14 = M15
			M15 = M08
			M08 = M01
			M01 = Temp
		end
	end

	if IsFull then
		local Result = buffer.create(EXTENDED_CV_SIZE)
		buffer.writeu32(Result, 0, bit32.bxor(V00, V08))
		buffer.writeu32(Result, 4, bit32.bxor(V01, V09))
		buffer.writeu32(Result, 8, bit32.bxor(V02, V10))
		buffer.writeu32(Result, 12, bit32.bxor(V03, V11))
		buffer.writeu32(Result, 16, bit32.bxor(V04, V12))
		buffer.writeu32(Result, 20, bit32.bxor(V05, V13))
		buffer.writeu32(Result, 24, bit32.bxor(V06, V14))
		buffer.writeu32(Result, 28, bit32.bxor(V07, V15))

		buffer.writeu32(Result, 32, bit32.bxor(V08, Hash00))
		buffer.writeu32(Result, 36, bit32.bxor(V09, Hash01))
		buffer.writeu32(Result, 40, bit32.bxor(V10, Hash02))
		buffer.writeu32(Result, 44, bit32.bxor(V11, Hash03))
		buffer.writeu32(Result, 48, bit32.bxor(V12, Hash04))
		buffer.writeu32(Result, 52, bit32.bxor(V13, Hash05))
		buffer.writeu32(Result, 56, bit32.bxor(V14, Hash06))
		buffer.writeu32(Result, 60, bit32.bxor(V15, Hash07))

		return Result
	else
		local Result = buffer.create(CV_SIZE)
		buffer.writeu32(Result, 0, bit32.bxor(V00, V08))
		buffer.writeu32(Result, 4, bit32.bxor(V01, V09))
		buffer.writeu32(Result, 8, bit32.bxor(V02, V10))
		buffer.writeu32(Result, 12, bit32.bxor(V03, V11))
		buffer.writeu32(Result, 16, bit32.bxor(V04, V12))
		buffer.writeu32(Result, 20, bit32.bxor(V05, V13))
		buffer.writeu32(Result, 24, bit32.bxor(V06, V14))
		buffer.writeu32(Result, 28, bit32.bxor(V07, V15))

		return Result
	end
end

local function ProcessMessage(InitialHashVector: buffer, Flags: number, Message: buffer, Length: number): buffer
	local MessageLength = buffer.len(Message)
	local StateCvs = buffer.create(STACK_BUFFER_SIZE)
	local StackSize = 0
	local StateCv = buffer.create(CV_SIZE)
	buffer.copy(StateCv, 0, InitialHashVector, 0, CV_SIZE)

	local StateCounter = 0
	local StateChunkNumber = 0
	local StateEndFlag = 0
	local StateStartFlag = CHUNK_START

	local BlockBuffer = buffer.create(BLOCK_SIZE)

	for BlockOffset = 0, MessageLength - BLOCK_SIZE - 1, BLOCK_SIZE do
		buffer.copy(BlockBuffer, 0, Message, BlockOffset, BLOCK_SIZE)
		local StateFlags = Flags + StateStartFlag + StateEndFlag

		StateCv = Compress(StateCv, BlockBuffer, StateCounter, BLOCK_SIZE, StateFlags)
		StateStartFlag = 0
		StateChunkNumber += 1

		if StateChunkNumber == 15 then
			StateEndFlag = CHUNK_END
		elseif StateChunkNumber == 16 then
			local MergeCv = StateCv
			local MergeAmount = StateCounter + 1

			while MergeAmount % 2 == 0 do
				StackSize = StackSize - 1
				local PopCv = buffer.create(CV_SIZE)
				buffer.copy(PopCv, 0, StateCvs, StackSize * CV_SIZE, CV_SIZE)

				local Block = buffer.create(EXTENDED_CV_SIZE)
				buffer.copy(Block, 0, PopCv, 0, CV_SIZE)
				buffer.copy(Block, CV_SIZE, MergeCv, 0, CV_SIZE)

				MergeCv = Compress(InitialHashVector, Block, 0, BLOCK_SIZE, Flags + PARENT_FLAG)
				MergeAmount = MergeAmount / 2
			end

			buffer.copy(StateCvs, StackSize * CV_SIZE, MergeCv, 0, CV_SIZE)
			StackSize = StackSize + 1
			buffer.copy(StateCv, 0, InitialHashVector, 0, CV_SIZE)
			StateStartFlag = CHUNK_START

			StateCounter += 1
			StateChunkNumber = 0
			StateEndFlag = 0
		end
	end

	local LastLength = MessageLength == 0 and 0 or ((MessageLength - 1) % BLOCK_SIZE + 1)
	local PaddedMessage = buffer.create(BLOCK_SIZE)

	if LastLength > 0 then
		buffer.copy(PaddedMessage, 0, Message, MessageLength - LastLength, LastLength)
	end

	local OutputCv: buffer
	local OutputBlock: buffer
	local OutputLength: number
	local OutputFlags: number

	if StateCounter > 0 then
		local StateFlags = Flags + StateStartFlag + CHUNK_END
		local MergeCv = Compress(StateCv, PaddedMessage, StateCounter, LastLength, StateFlags)

		for Index = StackSize, 2, -1 do
			local StackCv = buffer.create(CV_SIZE)
			buffer.copy(StackCv, 0, StateCvs, (Index - 1) * CV_SIZE, CV_SIZE)

			local Block = buffer.create(EXTENDED_CV_SIZE)
			buffer.copy(Block, 0, StackCv, 0, CV_SIZE)
			buffer.copy(Block, CV_SIZE, MergeCv, 0, CV_SIZE)

			MergeCv = Compress(InitialHashVector, Block, 0, BLOCK_SIZE, Flags + PARENT_FLAG)
		end

		OutputCv = InitialHashVector
		local FirstStackCv = buffer.create(CV_SIZE)
		buffer.copy(FirstStackCv, 0, StateCvs, 0, CV_SIZE)

		OutputBlock = buffer.create(EXTENDED_CV_SIZE)
		buffer.copy(OutputBlock, 0, FirstStackCv, 0, CV_SIZE)
		buffer.copy(OutputBlock, CV_SIZE, MergeCv, 0, CV_SIZE)

		OutputLength = BLOCK_SIZE
		OutputFlags = Flags + ROOT_FLAG + PARENT_FLAG
	else
		OutputCv = StateCv
		OutputBlock = PaddedMessage
		OutputLength = LastLength
		OutputFlags = Flags + StateStartFlag + CHUNK_END + ROOT_FLAG
	end

	local Output = buffer.create(Length)
	local OutputOffset = 0

	for Index = 0, Length // BLOCK_SIZE do
		local MessageDigest = Compress(OutputCv, OutputBlock, Index, OutputLength, OutputFlags, true)

		local BytesToCopy = math.min(BLOCK_SIZE, Length - OutputOffset)
		buffer.copy(Output, OutputOffset, MessageDigest, 0, BytesToCopy)
		OutputOffset += BytesToCopy

		if OutputOffset >= Length then
			break
		end
	end

	return Output
end

return function(Message: buffer, Length: number?): buffer
	return ProcessMessage(INITIAL_VECTORS, 0, Message, Length or 32)
end


--[=[
	Cryptography library: ChaCha20

	Sizes:
		Nonce: 12 bytes
		Key: 16/32 bytes

	Return type: buffer
	Example usage:
		local Data = buffer.fromstring("Hello World")
		local Key = buffer.fromstring(string.rep("k", 32))
		local Nonce = buffer.fromstring(string.rep("n", 12))

		--------Usage Case 1--------
		local Encrypted = ChaCha20(Data, Key, Nonce)

		--------Usage Case 2--------
		local Decrypted = ChaCha20(Encrypted, Key, Nonce)
--]=]

--!strict
--!native
--!optimize 2

local DWORD = 4
local BLOCK_SIZE = 64
local STATE_SIZE = 16

local CHACHA20_NONCE_SIZE = 12
local CHACHA20_KEY_SIZE_16 = 16
local CHACHA20_KEY_SIZE_32 = 32

local SIGMA_CONSTANTS = buffer.create(16) do
	local SigmaBytes = { string.byte("expand 32-byte k", 1, -1) }
	for Index, ByteValue in SigmaBytes do
		buffer.writeu8(SIGMA_CONSTANTS, Index - 1, ByteValue)
	end
end

local TAU_CONSTANTS = buffer.create(16) do
	local TauBytes = { string.byte("expand 16-byte k", 1, -1) }
	for Index, ByteValue in TauBytes do
		buffer.writeu8(TAU_CONSTANTS, Index - 1, ByteValue)
	end
end

local function ProcessBlock(InitialState: buffer, Rounds: number)
	local S0: number, S1: number, S2: number, S3: number, S4: number, S5: number, S6: number, S7: number, S8: number, S9: number, S10: number, S11: number, S12: number, S13: number, S14: number, S15: number =
		buffer.readu32(InitialState, 0), buffer.readu32(InitialState, 4),
		buffer.readu32(InitialState, 8), buffer.readu32(InitialState, 12),
		buffer.readu32(InitialState, 16), buffer.readu32(InitialState, 20),
		buffer.readu32(InitialState, 24), buffer.readu32(InitialState, 28),
		buffer.readu32(InitialState, 32), buffer.readu32(InitialState, 36),
		buffer.readu32(InitialState, 40), buffer.readu32(InitialState, 44),
		buffer.readu32(InitialState, 48), buffer.readu32(InitialState, 52),
		buffer.readu32(InitialState, 56), buffer.readu32(InitialState, 60)

	for Round = 1, Rounds do
		local IsOddRound = Round % 2 == 1

		if IsOddRound then
			S0 = bit32.bor(S0 + S4, 0); S12 = bit32.lrotate(bit32.bxor(S12, S0), 16)
			S8 = bit32.bor(S8 + S12, 0); S4 = bit32.lrotate(bit32.bxor(S4, S8), 12)
			S0 = bit32.bor(S0 + S4, 0); S12 = bit32.lrotate(bit32.bxor(S12, S0), 8)
			S8 = bit32.bor(S8 + S12, 0); S4 = bit32.lrotate(bit32.bxor(S4, S8), 7)

			S1 = bit32.bor(S1 + S5, 0); S13 = bit32.lrotate(bit32.bxor(S13, S1), 16)
			S9 = bit32.bor(S9 + S13, 0); S5 = bit32.lrotate(bit32.bxor(S5, S9), 12)
			S1 = bit32.bor(S1 + S5, 0); S13 = bit32.lrotate(bit32.bxor(S13, S1), 8)
			S9 = bit32.bor(S9 + S13, 0); S5 = bit32.lrotate(bit32.bxor(S5, S9), 7)

			S2 = bit32.bor(S2 + S6, 0); S14 = bit32.lrotate(bit32.bxor(S14, S2), 16)
			S10 = bit32.bor(S10 + S14, 0); S6 = bit32.lrotate(bit32.bxor(S6, S10), 12)
			S2 = bit32.bor(S2 + S6, 0); S14 = bit32.lrotate(bit32.bxor(S14, S2), 8)
			S10 = bit32.bor(S10 + S14, 0); S6 = bit32.lrotate(bit32.bxor(S6, S10), 7)

			S3 = bit32.bor(S3 + S7, 0); S15 = bit32.lrotate(bit32.bxor(S15, S3), 16)
			S11 = bit32.bor(S11 + S15, 0); S7 = bit32.lrotate(bit32.bxor(S7, S11), 12)
			S3 = bit32.bor(S3 + S7, 0); S15 = bit32.lrotate(bit32.bxor(S15, S3), 8)
			S11 = bit32.bor(S11 + S15, 0); S7 = bit32.lrotate(bit32.bxor(S7, S11), 7)
		else
			S0 = bit32.bor(S0 + S5, 0); S15 = bit32.lrotate(bit32.bxor(S15, S0), 16)
			S10 = bit32.bor(S10 + S15, 0); S5 = bit32.lrotate(bit32.bxor(S5, S10), 12)
			S0 = bit32.bor(S0 + S5, 0); S15 = bit32.lrotate(bit32.bxor(S15, S0), 8)
			S10 = bit32.bor(S10 + S15, 0); S5 = bit32.lrotate(bit32.bxor(S5, S10), 7)

			S1 = bit32.bor(S1 + S6, 0); S12 = bit32.lrotate(bit32.bxor(S12, S1), 16)
			S11 = bit32.bor(S11 + S12, 0); S6 = bit32.lrotate(bit32.bxor(S6, S11), 12)
			S1 = bit32.bor(S1 + S6, 0); S12 = bit32.lrotate(bit32.bxor(S12, S1), 8)
			S11 = bit32.bor(S11 + S12, 0); S6 = bit32.lrotate(bit32.bxor(S6, S11), 7)

			S2 = bit32.bor(S2 + S7, 0); S13 = bit32.lrotate(bit32.bxor(S13, S2), 16)
			S8 = bit32.bor(S8 + S13, 0); S7 = bit32.lrotate(bit32.bxor(S7, S8), 12)
			S2 = bit32.bor(S2 + S7, 0); S13 = bit32.lrotate(bit32.bxor(S13, S2), 8)
			S8 = bit32.bor(S8 + S13, 0); S7 = bit32.lrotate(bit32.bxor(S7, S8), 7)

			S3 = bit32.bor(S3 + S4, 0); S14 = bit32.lrotate(bit32.bxor(S14, S3), 16)
			S9 = bit32.bor(S9 + S14, 0); S4 = bit32.lrotate(bit32.bxor(S4, S9), 12)
			S3 = bit32.bor(S3 + S4, 0); S14 = bit32.lrotate(bit32.bxor(S14, S3), 8)
			S9 = bit32.bor(S9 + S14, 0); S4 = bit32.lrotate(bit32.bxor(S4, S9), 7)
		end
	end

	buffer.writeu32(InitialState, 0, buffer.readu32(InitialState, 0) + S0)
	buffer.writeu32(InitialState, 4, buffer.readu32(InitialState, 4) + S1)
	buffer.writeu32(InitialState, 8, buffer.readu32(InitialState, 8) + S2)
	buffer.writeu32(InitialState, 12, buffer.readu32(InitialState, 12) + S3)
	buffer.writeu32(InitialState, 16, buffer.readu32(InitialState, 16) + S4)
	buffer.writeu32(InitialState, 20, buffer.readu32(InitialState, 20) + S5)
	buffer.writeu32(InitialState, 24, buffer.readu32(InitialState, 24) + S6)
	buffer.writeu32(InitialState, 28, buffer.readu32(InitialState, 28) + S7)
	buffer.writeu32(InitialState, 32, buffer.readu32(InitialState, 32) + S8)
	buffer.writeu32(InitialState, 36, buffer.readu32(InitialState, 36) + S9)
	buffer.writeu32(InitialState, 40, buffer.readu32(InitialState, 40) + S10)
	buffer.writeu32(InitialState, 44, buffer.readu32(InitialState, 44) + S11)
	buffer.writeu32(InitialState, 48, buffer.readu32(InitialState, 48) + S12)
	buffer.writeu32(InitialState, 52, buffer.readu32(InitialState, 52) + S13)
	buffer.writeu32(InitialState, 56, buffer.readu32(InitialState, 56) + S14)
	buffer.writeu32(InitialState, 60, buffer.readu32(InitialState, 60) + S15)
end

local function InitializeState(Key: buffer, Nonce: buffer, Counter: number): buffer
	local KeyLength = buffer.len(Key)
	local State = buffer.create(STATE_SIZE * DWORD)

	local Constants = KeyLength == 32 and SIGMA_CONSTANTS or TAU_CONSTANTS

	buffer.copy(State, 0, Constants, 0, 16)

	buffer.copy(State, 16, Key, 0, math.min(KeyLength, 16))
	if KeyLength == 32 then
		buffer.copy(State, 32, Key, 16, 16)
	else
		buffer.copy(State, 32, Key, 0, 16)
	end

	buffer.writeu32(State, 48, Counter)
	buffer.copy(State, 52, Nonce, 0, 12)

	return State
end

local function ChaCha20(Data: buffer, Key: buffer, Nonce: buffer, Counter: number?, Rounds: number?): buffer
	if Data == nil then
		error("Data cannot be nil", 2)
	end

	if typeof(Data) ~= "buffer" then
		error(`Data must be a buffer, got {typeof(Data)}`, 2)
	end

	if Key == nil then
		error("Key cannot be nil", 2)
	end

	if typeof(Key) ~= "buffer" then
		error(`Key must be a buffer, got {typeof(Key)}`, 2)
	end

	local KeyLength = buffer.len(Key)
	if KeyLength ~= CHACHA20_KEY_SIZE_16 and KeyLength ~= CHACHA20_KEY_SIZE_32 then
		error(`Key must be {CHACHA20_KEY_SIZE_16} or {CHACHA20_KEY_SIZE_32} bytes long, got {KeyLength} bytes`, 2)
	end

	if Nonce == nil then
		error("Nonce cannot be nil", 2)
	end

	if typeof(Nonce) ~= "buffer" then
		error(`Nonce must be a buffer, got {typeof(Nonce)}`, 2)
	end

	local NonceLength = buffer.len(Nonce)
	if NonceLength ~= CHACHA20_NONCE_SIZE then
		error(`Nonce must be exactly {CHACHA20_NONCE_SIZE} bytes long, got {NonceLength} bytes`, 2)
	end

	if Counter then
		if typeof(Counter) ~= "number" then
			error(`Counter must be a number, got {typeof(Counter)}`, 2)
		end

		if Counter < 0 then
			error(`Counter cannot be negative, got {Counter}`, 2)
		end

		if Counter ~= math.floor(Counter) then
			error(`Counter must be an integer, got {Counter}`, 2)
		end

		if Counter >= 2^32 then
			error(`Counter must be less than 2^32, got {Counter}`, 2)
		end
	end

	if Rounds then
		if typeof(Rounds) ~= "number" then
			error(`Rounds must be a number, got {typeof(Rounds)}`, 2)
		end

		if Rounds <= 0 then
			error(`Rounds must be positive, got {Rounds}`, 2)
		end

		if Rounds ~= math.floor(Rounds) then
			error(`Rounds must be an integer, got {Rounds}`, 2)
		end

		if Rounds % 2 ~= 0 then
			error(`Rounds must be even, got {Rounds}`, 2)
		end
	end

	local BlockCounter = Counter or 1
	local BlockRounds = Rounds or 20

	local DataLength = buffer.len(Data)
	if DataLength == 0 then
		return buffer.create(0)
	end

	local Output = buffer.create(DataLength)

	local DataOffset = 0

	local State = InitializeState(Key, Nonce, BlockCounter)
	local StateBackup = buffer.create(64)
	buffer.copy(StateBackup, 0, State, 0)

	while DataOffset < DataLength do
		ProcessBlock(State, BlockRounds)

		local BytesToProcess = math.min(BLOCK_SIZE, DataLength - DataOffset)

		for Index = 0, BytesToProcess - 1 do
			local DataByte = buffer.readu8(Data, DataOffset + Index)
			local KeystreamByte = buffer.readu8(State, Index)
			buffer.writeu8(Output, DataOffset + Index, bit32.bxor(DataByte, KeystreamByte))
		end

		DataOffset += BytesToProcess
		BlockCounter += 1
		buffer.copy(State, 0, StateBackup, 0)
		buffer.writeu32(State, 48, BlockCounter)
	end

	return Output
end

return ChaCha20


--[=[
	Cryptography library: Cryptographically Secure RNG

	Usage:
		local RandomFloat = CSPRNG.Random()
		local RandomInt = CSPRNG.RandomInt(1, 100)
		local RandomNumber = CSPRNG.RandomNumber(0.5, 10.5)
		local RandomBytes = CSPRNG.RandomBytes(32)
		local RandomHex = CSPRNG.RandomHex(16)
		local FastString = CSPRNG.RandomString(16, false)
		local FastBuffer = CSPRNG.RandomString(32, true)

		local Ed25519Clamped = CSPRNG.Ed25519ClampedBytes(SomeBuffer)
		local Ed25519Random = CSPRNG.Ed25519Random()

		CSPRNG.AddEntropyProvider(function)
		CSPRNG.RemoveEntropyProvider(function)
		CSPRNG.Reseed()

		CSPRNG.BytesLeft
--]=]

--!native
--!optimize 2
--!strict

local Conversions = require("@self/Conversions")
local ChaCha20 = require("@self/ChaCha20")
local Blake3 = require("@self/Blake3")

export type EntropyProvider = (BytesLeft: number) -> buffer?

type CSPRNGModule = {
	BlockExpansion: boolean,

	SizeTarget: number,
	RekeyAfter: number,

	Key: buffer,
	Nonce: buffer,
	Buffer: buffer,

	Counter: number,
	BufferPosition: number,
	BufferSize: number,
	BytesLeft: number,

	EntropyProviders: { EntropyProvider },

	Reseed: (CustomEntropy: buffer?) -> (),
	AddEntropyProvider: (ProviderFunction: EntropyProvider) -> (),
	RemoveEntropyProvider: (ProviderFunction: EntropyProvider) -> (),

	Random: () -> number,
	RandomInt: (Min: number, Max: number?) -> number,
	RandomNumber: (Min: number, Max: number?) -> number,
	RandomBytes: (Count: number) -> buffer,
	RandomString: (Length: number, AsBuffer: boolean?) -> string | buffer,
	RandomHex: (Length: number) -> string,
	Ed25519ClampedBytes: (Input: buffer) -> buffer,
	Ed25519Random: () -> buffer,
}

local BLOCK_SIZE = 64
local KEY_SIZE = 32
local NONCE_SIZE = 12

local CSPRNG: CSPRNGModule = {
	BlockExpansion = true,
	SizeTarget = 2048,
	RekeyAfter = 1024,

	Key = buffer.create(0),
	Nonce = buffer.create(0),
	Buffer = buffer.create(0),

	Counter = 0,
	BufferPosition = 0,
	BufferSize = 0,
	BytesLeft = 0,

	EntropyProviders = {}
} :: CSPRNGModule

local INPUT_BUFFER = buffer.create(BLOCK_SIZE)
local REKEY_THRESHOLD = math.max(math.floor(CSPRNG.RekeyAfter), 2)
local SIZE_TARGET_CLAMPED = math.clamp(math.floor(CSPRNG.SizeTarget), 64, 4294967295)

local function Reset()
	CSPRNG.Key = buffer.create(0)
	CSPRNG.Nonce = buffer.create(0)
	CSPRNG.Buffer = buffer.create(0)

	CSPRNG.Counter = 0
	CSPRNG.BufferPosition = 0
	CSPRNG.BufferSize = 0
end

local function GatherEntropy(CustomEntropy: buffer?): number
	local EntropyBuffers = buffer.create(1024)
	local Offset = 0

	local function WriteToBuffer(Source: buffer)
		local Size = buffer.len(Source)
		buffer.copy(EntropyBuffers, Offset, Source, 0, Size)
		Offset += Size
	end

	local CurrentTime = 1.234
	if tick then
		CurrentTime = tick()
		local TimeBuffer = buffer.create(8)
		buffer.writef64(TimeBuffer, 0, CurrentTime)
		WriteToBuffer(TimeBuffer)
	end

	local ClockTime = os.clock()
	local ClockBuffer = buffer.create(8)
	buffer.writef64(ClockBuffer, 0, ClockTime)
	WriteToBuffer(ClockBuffer)

	local UnixTime = os.time()
	local UnixBuffer = buffer.create(8)
	buffer.writeu32(UnixBuffer, 0, UnixTime % 0x100000000)
	buffer.writeu32(UnixBuffer, 4, math.floor(UnixTime / 0x100000000))
	WriteToBuffer(UnixBuffer)

	local DateTimeMillis = 5.678
	if DateTime then
		DateTimeMillis = DateTime.now().UnixTimestampMillis
		local DateTimeBuffer = buffer.create(8)
		buffer.writef64(DateTimeBuffer, 0, DateTimeMillis)
		WriteToBuffer(DateTimeBuffer)

		local DateTimePrecisionBuffer = buffer.create(16)
		buffer.writef32(DateTimePrecisionBuffer, 0, DateTimeMillis / 1000)
		buffer.writef32(DateTimePrecisionBuffer, 4, (DateTimeMillis % 1000) / 100)
		buffer.writef32(DateTimePrecisionBuffer, 8, DateTimeMillis / 86400000)
		buffer.writef32(DateTimePrecisionBuffer, 12, (DateTimeMillis * 0.001) % 1)
		WriteToBuffer(DateTimePrecisionBuffer)
	else
		WriteToBuffer(buffer.create(24))
	end

	local FracTimeBuffer = buffer.create(16)
	buffer.writef32(FracTimeBuffer, 0, ClockTime / 100)
	buffer.writef32(FracTimeBuffer, 4, CurrentTime / 1000)
	buffer.writef32(FracTimeBuffer, 8, (ClockTime * 12345.6789) % 1)
	buffer.writef32(FracTimeBuffer, 12, (CurrentTime * 98765.4321) % 1)
	WriteToBuffer(FracTimeBuffer)

	local NoiseBuffer = buffer.create(32)
	for Index = 0, 7 do
		local Noise1 = math.noise(ClockTime + Index, UnixTime + Index, ClockTime + UnixTime + Index)
		local Noise2 = math.noise(CurrentTime + Index * 0.1, DateTimeMillis * 0.0001 + Index, ClockTime * 1.5 + Index)
		local Noise3 = math.noise(UnixTime * 0.01 + Index, ClockTime + DateTimeMillis * 0.001, CurrentTime + Index * 2)
		local Noise4 = math.noise(DateTimeMillis * 0.00001 + Index, UnixTime + ClockTime + Index, CurrentTime * 0.1 + Index)

		buffer.writef32(NoiseBuffer, Index * 4, Noise1 + Noise2 + Noise3 + Noise4)
	end
	WriteToBuffer(NoiseBuffer)

	local BenchmarkTimings = buffer.create(32)
	for Index = 0, 7 do
		local StartTime = os.clock()
		local Sum = 0

		local Iterations = 50 + (Index * 25)
		for Iteration = 1, Iterations do
			Sum += Iteration * Iteration + math.sin(Iteration / 10) * math.cos(Iteration / 7)
		end

		local EndTime = os.clock()
		local TimingDelta = EndTime - StartTime
		buffer.writef32(BenchmarkTimings, Index * 4, TimingDelta * 1000000)
	end
	WriteToBuffer(BenchmarkTimings)

	local AllocTimings = buffer.create(24)
	for Index = 0, 5 do
		local AllocStart = os.clock()

		for AllocIndex = 1, 20 do
			local _TempBuf = buffer.create(64 + AllocIndex)
		end

		local AllocEnd = os.clock()
		buffer.writef32(AllocTimings, Index * 4, (AllocEnd - AllocStart) * 10000000)
	end
	WriteToBuffer(AllocTimings)

	local MicroTime = math.floor(CurrentTime * 1000000)
	local MicroTimeBuffer = buffer.create(8)
	buffer.writeu32(MicroTimeBuffer, 0, MicroTime % 0x100000000)
	buffer.writeu32(MicroTimeBuffer, 4, math.floor(MicroTime / 0x100000000))
	WriteToBuffer(MicroTimeBuffer)

	if game then
		if game.JobId and #game.JobId > 0 then
			local JobIdBuffer = buffer.fromstring(game.JobId)
			WriteToBuffer(JobIdBuffer)
		end

		if game.PlaceId then
			local PlaceIdBuffer = buffer.create(8)
			buffer.writeu32(PlaceIdBuffer, 0, game.PlaceId % 0x100000000)
			buffer.writeu32(PlaceIdBuffer, 4, math.floor(game.PlaceId / 0x100000000))
			WriteToBuffer(PlaceIdBuffer)
		end

		if workspace and workspace.DistributedGameTime then
			local DistTimeBuffer = buffer.create(8)
			buffer.writef64(DistTimeBuffer, 0, workspace.DistributedGameTime)
			WriteToBuffer(DistTimeBuffer)

			local DistMicroTime = math.floor(workspace.DistributedGameTime * 1000000)
			local DistMicroBuffer = buffer.create(8)
			buffer.writeu32(DistMicroBuffer, 0, DistMicroTime % 0x100000000)
			buffer.writeu32(DistMicroBuffer, 4, math.floor(DistMicroTime / 0x100000000))
			WriteToBuffer(DistMicroBuffer)
		end
	end

	local AddressEntropy = buffer.create(128)
	for Index = 0, 7 do
		local TempTable = {}
		local TempFunc = function() end
		local TempBuffer = buffer.create(0)
		local TempUserdata = newproxy()

		local TableAddr = string.gsub(tostring(TempTable), "table: ", "")
		local FuncAddr = string.gsub(tostring(TempFunc), "function: ", "")
		local BufferAddr = string.gsub(tostring(TempBuffer), "buffer: ", "")
		local UserdataAddr = string.gsub(tostring(TempUserdata), "userdata: ", "")

		local TableHash = 0
		local ThreadHash = 0
		local FuncHash = 0
		local BufferHash = 0
		local UserdataHash = 0

		for AddrIndex = 1, #TableAddr do
			TableHash = bit32.bxor(TableHash, string.byte(TableAddr, AddrIndex)) * 31
		end

		if coroutine then
			local ThreadAddr = string.gsub(tostring(coroutine.create(function() end)), "thread: ", "")
			for AddrIndex = 1, #ThreadAddr do
				ThreadHash = bit32.bxor(ThreadHash, string.byte(ThreadAddr, AddrIndex)) * 31
			end
		end

		for AddrIndex = 1, #FuncAddr do
			FuncHash = bit32.bxor(FuncHash, string.byte(FuncAddr, AddrIndex)) * 37
		end
		for AddrIndex = 1, #BufferAddr do
			BufferHash = bit32.bxor(BufferHash, string.byte(BufferAddr, AddrIndex)) * 41
		end
		for AddrIndex = 1, #UserdataAddr do
			UserdataHash = bit32.bxor(UserdataHash, string.byte(UserdataAddr, AddrIndex)) * 43
		end

		buffer.writeu32(AddressEntropy, Index * 16, TableHash)
		buffer.writeu32(AddressEntropy, Index * 16 + 4, ThreadHash)
		buffer.writeu32(AddressEntropy, Index * 16 + 8, FuncHash)
		buffer.writeu32(AddressEntropy, Index * 16 + 12, bit32.bxor(BufferHash, UserdataHash))
	end
	WriteToBuffer(AddressEntropy)

	local function AddExtraEntropy(Entropy: buffer?, Warn: boolean, Provider: string?)
		if not Entropy then
			return
		end

		local BytesLeft = 1024 - Offset

		if BytesLeft > 0 then
			local Extra = buffer.len(Entropy) - BytesLeft
			local Truncated = math.min(BytesLeft, buffer.len(Entropy))

			if Extra > 0 and Warn and Provider then
				warn(`CSPRNG: {Provider} returned {Extra} bytes more than available and was truncated to {Truncated} bytes`)
			end

			buffer.copy(EntropyBuffers, Offset, Entropy, 0, Truncated)
		end
	end

	for Index, Provider in CSPRNG.EntropyProviders do
		local BytesLeft = 1024 - Offset
		if BytesLeft > 0 then
			local Success: boolean, ExtraEntropy: buffer? = pcall(Provider, BytesLeft)
			if not Success then
				warn(`CSPRNG Provider errored with {ExtraEntropy}`)
			end

			AddExtraEntropy(ExtraEntropy, true, `Entropy Provider #{Index}`)
		end
	end

	if CustomEntropy then
		AddExtraEntropy(CustomEntropy, false)
	end

	local KeyMaterial = Blake3(EntropyBuffers, KEY_SIZE + NONCE_SIZE)

	CSPRNG.Key = buffer.create(KEY_SIZE)
	buffer.copy(CSPRNG.Key, 0, KeyMaterial, 0, KEY_SIZE)

	CSPRNG.Nonce = buffer.create(NONCE_SIZE)
	buffer.copy(CSPRNG.Nonce, 0, KeyMaterial, KEY_SIZE, NONCE_SIZE)

	return buffer.len(EntropyBuffers) - Offset
end

local function GenerateBlock()
	buffer.fill(INPUT_BUFFER, 0, 0, BLOCK_SIZE)
	local ChaChaOutput = ChaCha20(INPUT_BUFFER, CSPRNG.Key, CSPRNG.Nonce, CSPRNG.Counter, 20)

	CSPRNG.Buffer = if CSPRNG.BlockExpansion then Blake3(ChaChaOutput, SIZE_TARGET_CLAMPED) else ChaChaOutput
	CSPRNG.BufferPosition = 0
	CSPRNG.BufferSize = buffer.len(CSPRNG.Buffer)
	CSPRNG.Counter += 1

	if CSPRNG.Counter % REKEY_THRESHOLD == 0 then
		GatherEntropy()
		CSPRNG.Counter = 0
	end
end

local function GetBytes(Count: number): buffer
	local Result = buffer.create(Count)
	local ResultPosition = 0

	while ResultPosition < Count do
		if CSPRNG.BufferPosition >= CSPRNG.BufferSize then
			GenerateBlock()
		end

		local BytesNeeded = Count - ResultPosition
		local BytesAvailable = CSPRNG.BufferSize - CSPRNG.BufferPosition
		local BytesToCopy = math.min(BytesNeeded, BytesAvailable)

		buffer.copy(Result, ResultPosition, CSPRNG.Buffer, CSPRNG.BufferPosition, BytesToCopy)
		ResultPosition += BytesToCopy
		CSPRNG.BufferPosition += BytesToCopy
	end

	return Result
end

local function GetFloat(): number
	if CSPRNG.BufferPosition + 8 > CSPRNG.BufferSize then
		GenerateBlock()
	end

	local Value1 = buffer.readu32(CSPRNG.Buffer, CSPRNG.BufferPosition)
	local Value2 = buffer.readu32(CSPRNG.Buffer, CSPRNG.BufferPosition + 4)
	CSPRNG.BufferPosition += 8

	local High = bit32.rshift(Value1, 5)
	local Low = bit32.rshift(Value2, 6)

	return (High * 67108864.0 + Low) / 9007199254740992.0
end

local function GetIntRange(Min: number, Max: number): number
	local Range = Max - Min + 1
	local MaxUInt32 = 0xFFFFFFFF
	local Limit = MaxUInt32 - (MaxUInt32 % Range)

	if CSPRNG.BufferPosition + 4 > CSPRNG.BufferSize then
		GenerateBlock()
	end

	local Value = buffer.readu32(CSPRNG.Buffer, CSPRNG.BufferPosition)
	CSPRNG.BufferPosition += 4

	if bit32.band(Range, Range - 1) == 0 then
		return Min + bit32.band(Value, Range - 1)
	else
		while Value > Limit do
			if CSPRNG.BufferPosition + 4 > CSPRNG.BufferSize then
				GenerateBlock()
			end
			Value = buffer.readu32(CSPRNG.Buffer, CSPRNG.BufferPosition)
			CSPRNG.BufferPosition += 4
		end

		return Min + (Value % Range)
	end
end

local function GetNumberRange(Min: number, Max: number): number
	if Min > Max then
		Min, Max = Max, Min
	end

	local Range = Max - Min
	if Range <= 0 then
		return Min
	end

	return Min + (GetFloat() * Range)
end

local function GetRandomString(Length: number, AsBuffer: boolean?): string | buffer
	local Characters = buffer.create(Length)

	for Index = 0, Length - 1 do
		buffer.writeu8(Characters, Index, GetIntRange(36, 122))
	end

	return if AsBuffer
		then Characters
		else buffer.tostring(Characters)
end

local function GetEd25519RandomBytes(): buffer
	local Output = buffer.create(32)

	for Index = 0, 31 do
		buffer.writeu8(Output, Index, GetIntRange(0, 255))
	end

	return Output
end

local function GetEd25519ClampedBytes(Input: buffer): buffer
	local Output = buffer.create(32)
	buffer.copy(Output, 0, Input, 0, 32)

	local FirstByte = buffer.readu8(Output, 0)
	FirstByte = bit32.band(FirstByte, 0xF8)
	buffer.writeu8(Output, 0, FirstByte)

	local LastByte = buffer.readu8(Output, 31)
	LastByte = bit32.band(LastByte, 0x7F)
	LastByte = bit32.bor(LastByte, 0x40)
	buffer.writeu8(Output, 31, LastByte)

	local HasVariation = false
	local FirstMiddleByte = buffer.readu8(Output, 1)
	for Index = 2, 30 do
		if buffer.readu8(Output, Index) ~= FirstMiddleByte then
			HasVariation = true
			break
		end
	end

	if not HasVariation then
		buffer.writeu8(Output, 15, bit32.bxor(FirstMiddleByte, 0x55))
	end

	return Output
end

local function GetHexString(Length: number): string
	local BytesNeeded = Length / 2
	local Bytes = GetBytes(BytesNeeded)
	local Hex = Conversions.ToHex(Bytes)

	return Hex
end

function CSPRNG.AddEntropyProvider(ProviderFunction: EntropyProvider)
	table.insert(CSPRNG.EntropyProviders, ProviderFunction)
end

function CSPRNG.RemoveEntropyProvider(ProviderFunction: EntropyProvider)
	for Index = #CSPRNG.EntropyProviders, 1, -1 do
		if CSPRNG.EntropyProviders[Index] == ProviderFunction then
			table.remove(CSPRNG.EntropyProviders, Index)
			break
		end
	end
end

function CSPRNG.Random(): number
	return GetFloat()
end

function CSPRNG.RandomInt(Min: number, Max: number?): number
	if Max and type(Max) ~= "number" then
		error(`Max must be a number or nil, got {typeof(Max)}`, 2)
	end

	if type(Min) ~= "number" then
		error(`Min must be a number, got {typeof(Min)}`, 2)
	end

	if Max and Max < Min then
		error(`Max ({Max}) can't be less than Min ({Min})`, 2)
	end

	if Max and Max == Min then
		error(`Max ({Max}) can't be equal to Min ({Min})`, 2)
	end

	local ActualMax: number
	local ActualMin: number

	if Max == nil then
		ActualMax = Min
		ActualMin = 1
	else
		ActualMax = Max
		ActualMin = Min
	end

	return GetIntRange(ActualMin, ActualMax)
end

function CSPRNG.RandomNumber(Min: number, Max: number?): number
	if Max and type(Max) ~= "number" then
		error(`Max must be a number or nil, got {typeof(Max)}`, 2)
	end

	if type(Min) ~= "number" then
		error(`Min must be a number, got {typeof(Min)}`, 2)
	end

	if Max and Max < Min then
		error(`Max ({Max}) must be bigger than Min ({Min})`, 2)
	end

	if Max and Max == Min then
		error(`Max ({Max}) can't be equal to Min ({Min})`, 2)
	end

	local ActualMax: number
	local ActualMin: number

	if Max == nil then
		ActualMax = Min
		ActualMin = 0
	else
		ActualMax = Max
		ActualMin = Min
	end

	return GetNumberRange(ActualMin, ActualMax)
end

function CSPRNG.RandomBytes(Count: number): buffer
	if type(Count) ~= "number" then
		error(`Count must be a number, got {typeof(Count)}`, 2)
	end

	if Count <= 0 then
		error(`Count must be bigger than 0, got {Count}`, 2)
	end

	if Count % 1 ~= 0 then
		error("Count must be an integer", 2)
	end

	return GetBytes(Count)
end

function CSPRNG.RandomString(Length: number, AsBuffer: boolean?): string | buffer
	if type(Length) ~= "number" then
		error(`Length must be a number, got {typeof(Length)}`, 2)
	end

	if Length <= 0 then
		error(`Length must be bigger than 0, got {Length}`, 2)
	end

	if Length % 1 ~= 0 then
		error("Length must be an integer", 2)
	end

	if AsBuffer ~= nil and type(AsBuffer) ~= "boolean" then
		error(`AsBuffer must be a boolean or nil, got {typeof(AsBuffer)}`, 2)
	end

	return GetRandomString(Length, AsBuffer)
end

function CSPRNG.RandomHex(Length: number): string
	if type(Length) ~= "number" then
		error(`Length must be a number, got {typeof(Length)}`, 2)
	end

	if Length <= 0 then
		error(`Length must be bigger than 0, got {Length}`, 2)
	end

	if Length % 1 ~= 0 then
		error("Length must be an integer", 2)
	end

	if Length % 2 ~= 0 then
		error(`Length must be even, got {Length}`, 2)
	end

	return GetHexString(Length)
end

function CSPRNG.Ed25519ClampedBytes(Input: buffer): buffer
	if type(Input) ~= "buffer" then
		error(`Input must be a buffer, got {typeof(Input)}`, 2)
	end

	return GetEd25519ClampedBytes(Input)
end

function CSPRNG.Ed25519Random(): buffer
	return GetEd25519ClampedBytes(GetEd25519RandomBytes())
end

function CSPRNG.Reseed(CustomEntropy: buffer?)
	if CustomEntropy ~= nil and type(CustomEntropy) ~= "buffer" then
		error(`CustomEntropy must be a buffer or nil, got {typeof(CustomEntropy)}`, 2)
	end

	Reset()
	GatherEntropy(CustomEntropy)
end

CSPRNG.BytesLeft = GatherEntropy()
GenerateBlock()

return CSPRNG


--[=[
	Cryptography library: Curve25519 Montgomery

	Return type: varies by function
	Example usage:
		local Curve25519 = require("Curve25519")
		local FieldQuadratic = require("FieldQuadratic")

		--------Usage Case 1: Point multiplication--------
		local BasePoint = Curve25519.G
		local SomeScalar = FieldQuadratic.Decode(ScalarBytes)
		local ScalarBits, BitCount = FieldQuadratic.Bits(SomeScalar)
		local Result = Curve25519.Ladder8(BasePoint, ScalarBits, BitCount)

		--------Usage Case 2: Encode/decode points--------
		local EncodedPoint = Curve25519.Encode(Curve25519.Scale(Result))
		local DecodedPoint = Curve25519.Decode(EncodedPoint)
--]=]

--!strict
--!optimize 2
--!native

local FieldPrime = require("./FieldPrime")
local CSPRNG = require("./CSPRNG")

local MONTGOMERY_POINT_SIZE = 208
local COORD_SIZE = 104

local function GetMontgomeryCoord(Point: buffer, Index: number): buffer
	local Coord = buffer.create(COORD_SIZE)
	buffer.copy(Coord, 0, Point, Index * COORD_SIZE, COORD_SIZE)

	return Coord
end

local function Double(PointToDouble: buffer): buffer
	local CoordX = GetMontgomeryCoord(PointToDouble, 0)
	local CoordZ = GetMontgomeryCoord(PointToDouble, 1)

	local SumXZ = FieldPrime.Add(CoordX, CoordZ)
	local SumSquared = FieldPrime.Square(SumXZ)
	local DiffXZ = FieldPrime.Sub(CoordX, CoordZ)
	local DiffSquared = FieldPrime.Square(DiffXZ)
	local Difference = FieldPrime.Sub(SumSquared, DiffSquared)
	local NewX = FieldPrime.Mul(SumSquared, DiffSquared)
	local NewZ = FieldPrime.Mul(Difference, FieldPrime.Add(DiffSquared, FieldPrime.KMul(Difference, 121666)))

	local Point = buffer.create(MONTGOMERY_POINT_SIZE)
	buffer.copy(Point, 0 * COORD_SIZE, NewX, 0, COORD_SIZE)
	buffer.copy(Point, 1 * COORD_SIZE, NewZ, 0, COORD_SIZE)

	return Point
end

local function LadderStep(DifferencePoint: buffer, Point1: buffer, Point2: buffer): (buffer, buffer)
	local DiffX = GetMontgomeryCoord(DifferencePoint, 0)
	local DiffZ = GetMontgomeryCoord(DifferencePoint, 1)
	local X1 = GetMontgomeryCoord(Point1, 0)
	local Z1 = GetMontgomeryCoord(Point1, 1)
	local X2 = GetMontgomeryCoord(Point2, 0)
	local Z2 = GetMontgomeryCoord(Point2, 1)

	local SumA = FieldPrime.Add(X1, Z1)
	local SumSquaredAA = FieldPrime.Square(SumA)
	local DiffB = FieldPrime.Sub(X1, Z1)
	local DiffSquaredBB = FieldPrime.Square(DiffB)

	local DifferenceE = FieldPrime.Sub(SumSquaredAA, DiffSquaredBB)
	local DiffD = FieldPrime.Sub(X2, Z2)
	local CrossDA = FieldPrime.Mul(DiffD, SumA)
	local SumC = FieldPrime.Add(X2, Z2)
	local CrossCB = FieldPrime.Mul(SumC, DiffB)

	local NewX4 = FieldPrime.Mul(DiffZ, FieldPrime.Square(FieldPrime.Add(CrossDA, CrossCB)))
	local NewZ4 = FieldPrime.Mul(DiffX, FieldPrime.Square(FieldPrime.Sub(CrossDA, CrossCB)))
	local NewX3 = FieldPrime.Mul(SumSquaredAA, DiffSquaredBB)
	local NewZ3 = FieldPrime.Mul(DifferenceE, FieldPrime.Add(DiffSquaredBB, FieldPrime.KMul(DifferenceE, 121666)))

	local Point = buffer.create(MONTGOMERY_POINT_SIZE)
	buffer.copy(Point, 0 * COORD_SIZE, NewX3, 0, COORD_SIZE)
	buffer.copy(Point, 1 * COORD_SIZE, NewZ3, 0, COORD_SIZE)

	local SecondPoint = buffer.create(MONTGOMERY_POINT_SIZE)
	buffer.copy(SecondPoint, 0 * COORD_SIZE, NewX4, 0, COORD_SIZE)
	buffer.copy(SecondPoint, 1 * COORD_SIZE, NewZ4, 0, COORD_SIZE)

	return Point, SecondPoint
end

local function Ladder(DifferencePoint: buffer, ScalarBits: buffer, ScalarBitCount: number): buffer
	local CurrentP = buffer.create(MONTGOMERY_POINT_SIZE)
	buffer.copy(CurrentP, 0 * COORD_SIZE, FieldPrime.Num(1), 0, COORD_SIZE)
	buffer.copy(CurrentP, 1 * COORD_SIZE, FieldPrime.Num(0), 0, COORD_SIZE)

	local CurrentQ = buffer.create(MONTGOMERY_POINT_SIZE)
	buffer.copy(CurrentQ, 0, DifferencePoint, 0, MONTGOMERY_POINT_SIZE)

	local LadderStep = LadderStep

	for BitIndex = ScalarBitCount, 1, -1 do
		local BitValue = buffer.readf64(ScalarBits, (BitIndex - 1) * 8)
		if BitValue == 0 then
			CurrentP, CurrentQ = LadderStep(DifferencePoint, CurrentP, CurrentQ)
		else
			CurrentQ, CurrentP = LadderStep(DifferencePoint, CurrentQ, CurrentP)
		end
	end

	return CurrentP
end

local Curve25519 = {}

function Curve25519.DifferentialAdd(DifferencePoint: buffer, Point1: buffer, Point2: buffer): buffer
	local DiffX = GetMontgomeryCoord(DifferencePoint, 0)
	local DiffZ = GetMontgomeryCoord(DifferencePoint, 1)
	local X1 = GetMontgomeryCoord(Point1, 0)
	local Z1 = GetMontgomeryCoord(Point1, 1)
	local X2 = GetMontgomeryCoord(Point2, 0)
	local Z2 = GetMontgomeryCoord(Point2, 1)

	local SumA = FieldPrime.Add(X1, Z1)
	local DiffB = FieldPrime.Sub(X1, Z1)
	local SumC = FieldPrime.Add(X2, Z2)
	local DiffD = FieldPrime.Sub(X2, Z2)
	local CrossDA = FieldPrime.Mul(DiffD, SumA)
	local CrossCB = FieldPrime.Mul(SumC, DiffB)

	local NewX = FieldPrime.Mul(DiffZ, FieldPrime.Square(FieldPrime.Add(CrossDA, CrossCB)))
	local NewZ = FieldPrime.Mul(DiffX, FieldPrime.Square(FieldPrime.Sub(CrossDA, CrossCB)))

	local Point = buffer.create(MONTGOMERY_POINT_SIZE)
	buffer.copy(Point, 0 * COORD_SIZE, NewX, 0, COORD_SIZE)
	buffer.copy(Point, 1 * COORD_SIZE, NewZ, 0, COORD_SIZE)

	return Point
end

function Curve25519.Decode(EncodedBuffer: buffer): buffer
	local Point = buffer.create(MONTGOMERY_POINT_SIZE)
	buffer.copy(Point, 0 * COORD_SIZE, FieldPrime.Decode(EncodedBuffer), 0, COORD_SIZE)
	buffer.copy(Point, 1 * COORD_SIZE, FieldPrime.Num(1), 0, COORD_SIZE)

	return Point
end

function Curve25519.Prac(BasePoint: buffer, PracRuleset: {any}): (buffer?, buffer?, buffer?)
	local DifferentialAdd = Curve25519.DifferentialAdd
	local RandomBuffer = CSPRNG.Ed25519Random()
	local RandomFactor = FieldPrime.Decode(RandomBuffer)

	local BaseX = GetMontgomeryCoord(BasePoint, 0)
	local BaseZ = GetMontgomeryCoord(BasePoint, 1)
	local RandomizedA = buffer.create(MONTGOMERY_POINT_SIZE)
	buffer.copy(RandomizedA, 0 * COORD_SIZE, FieldPrime.Mul(BaseX, RandomFactor), 0, COORD_SIZE)
	buffer.copy(RandomizedA, 1 * COORD_SIZE, FieldPrime.Mul(BaseZ, RandomFactor), 0, COORD_SIZE)

	RandomizedA = Double(Double(Double(RandomizedA)))

	local AZ = GetMontgomeryCoord(RandomizedA, 1)
	if FieldPrime.Eqz(AZ) then
		return nil, nil, nil
	end

	RandomizedA = Ladder(RandomizedA, PracRuleset[1], PracRuleset[2])

	local Rules: buffer = PracRuleset[3]
	local RuleCount: number = PracRuleset[4]
	if RuleCount == 0 then
		return RandomizedA, nil, nil
	end

	local CurrentB, CurrentC
	local FirstRule = buffer.readf64(Rules, (RuleCount - 1) * 8)
	if FirstRule == 2 then
		local DoubledA = Double(RandomizedA)
		RandomizedA, CurrentB, CurrentC = DifferentialAdd(RandomizedA, DoubledA, RandomizedA), RandomizedA, DoubledA
	elseif FirstRule == 3 or FirstRule == 5 then
		RandomizedA, CurrentB, CurrentC = Double(RandomizedA), RandomizedA, RandomizedA
	elseif FirstRule == 6 then
		local DoubledA = Double(RandomizedA)
		local TripledA = DifferentialAdd(RandomizedA, DoubledA, RandomizedA)
		RandomizedA, CurrentB, CurrentC = Double(TripledA), RandomizedA, DifferentialAdd(RandomizedA, TripledA, DoubledA)
	elseif FirstRule == 7 then
		local DoubledA = Double(RandomizedA)
		local TripledA = DifferentialAdd(RandomizedA, DoubledA, RandomizedA)
		local QuadrupleA = Double(DoubledA)
		RandomizedA, CurrentB, CurrentC = DifferentialAdd(TripledA, QuadrupleA, RandomizedA), RandomizedA, QuadrupleA
	elseif FirstRule == 8 then
		local DoubledA = Double(RandomizedA)
		local TripledA = DifferentialAdd(RandomizedA, DoubledA, RandomizedA)
		RandomizedA, CurrentB, CurrentC = Double(DoubledA), RandomizedA, TripledA
	else
		RandomizedA, CurrentB, CurrentC = RandomizedA, Double(RandomizedA), RandomizedA
	end

	if not CurrentC then
		return nil, nil, nil
	end

	for RuleIndex = RuleCount - 1, 1, -1 do
		local CurrentRule = buffer.readf64(Rules, (RuleIndex - 1) * 8)
		if CurrentRule == 0 then
			RandomizedA, CurrentB = CurrentB, RandomizedA
		elseif CurrentRule == 1 then
			local SumAB = DifferentialAdd(CurrentC, RandomizedA, CurrentB)
			RandomizedA, CurrentB = DifferentialAdd(CurrentB, SumAB, RandomizedA), DifferentialAdd(RandomizedA, SumAB, CurrentB)
		elseif CurrentRule == 2 then
			RandomizedA, CurrentC = DifferentialAdd(CurrentB, DifferentialAdd(CurrentC, RandomizedA, CurrentB), RandomizedA), Double(RandomizedA)
		elseif CurrentRule == 3 then
			RandomizedA, CurrentC = DifferentialAdd(CurrentC, RandomizedA, CurrentB), RandomizedA
		elseif CurrentRule == 5 then
			RandomizedA, CurrentC = Double(RandomizedA), DifferentialAdd(CurrentB, RandomizedA, CurrentC)
		elseif CurrentRule == 6 then
			local SumAB = DifferentialAdd(CurrentC, RandomizedA, CurrentB)
			local DoubledSumAABB = Double(SumAB)
			RandomizedA, CurrentC = DifferentialAdd(SumAB, DoubledSumAABB, SumAB), DifferentialAdd(DifferentialAdd(RandomizedA, SumAB, CurrentB), DoubledSumAABB, RandomizedA)
		elseif CurrentRule == 7 then
			local SumAB = DifferentialAdd(CurrentC, RandomizedA, CurrentB)
			local DoubleAAB = DifferentialAdd(CurrentB, SumAB, RandomizedA)
			RandomizedA, CurrentC = DifferentialAdd(RandomizedA, DoubleAAB, SumAB), DifferentialAdd(SumAB, DoubleAAB, RandomizedA)
		elseif CurrentRule == 8 then
			local DoubledA = Double(RandomizedA)
			RandomizedA, CurrentC = DifferentialAdd(CurrentC, DoubledA, DifferentialAdd(CurrentC, RandomizedA, CurrentB)), DifferentialAdd(RandomizedA, DoubledA, RandomizedA)
		else
			CurrentB, CurrentC = Double(CurrentB), DifferentialAdd(RandomizedA, CurrentC, CurrentB)
		end
	end

	return RandomizedA, CurrentB, CurrentC
end

local Point = buffer.create(MONTGOMERY_POINT_SIZE)
buffer.copy(Point, 0 * COORD_SIZE, FieldPrime.Num(9), 0, COORD_SIZE)
buffer.copy(Point, 1 * COORD_SIZE, FieldPrime.Num(1), 0, COORD_SIZE)
Curve25519.G = Point

return Curve25519