diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8a53fe9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,20 @@ +name: test + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + matrix: + ENGINE_VERSION: [5.3.0, 5.4.0, 5.5.0, 5.6.1, latest] + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: recursive + - name: test + run: docker-compose up --exit-code-from test diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..aedc543 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: "3.6" + +services: + test: + build: ./test + user: root + volumes: + - "./:/root/.minetest/worlds/world/worldmods/otp/" + - "./test/minetest.conf:/minetest.conf" + - "world:/root/.minetest/worlds/world" + ports: + - "30000:30000/udp" + +volumes: + world: {} \ No newline at end of file diff --git a/functions.lua b/functions.lua index d83dd53..ccb57fe 100644 --- a/functions.lua +++ b/functions.lua @@ -1,6 +1,6 @@ -- https://stackoverflow.com/a/25594410 -function otp.bitXOR(a,b)--Bitwise xor +local function bitxor(a,b) local p,c=1,0 while a>0 and b>0 do local ra,rb=a%2,b%2 @@ -16,6 +16,16 @@ function otp.bitXOR(a,b)--Bitwise xor return c end +local function bitor(a,b) + local p,c=1,0 + while a+b>0 do + local ra,rb=a%2,b%2 + if ra+rb>0 then c=c+p end + a,b,p=(a-ra)/2,(b-rb)/2,p*2 + end + return c +end + -- https://stackoverflow.com/a/32387452 local function bitand(a, b) local result = 0 @@ -36,15 +46,20 @@ local function rshift(x, by) return math.floor(x / 2 ^ by) end -function otp.write_uint64(v) - local b1 = bitand(v, 0xFF) - local b2 = bitand( rshift(v, 8), 0xFF ) - local b3 = bitand( rshift(v, 16), 0xFF ) - local b4 = bitand( rshift(v, 24), 0xFF ) - local b5 = bitand( rshift(v, 32), 0xFF ) - local b6 = bitand( rshift(v, 40), 0xFF ) - local b7 = bitand( rshift(v, 48), 0xFF ) - local b8 = bitand( rshift(v, 56), 0xFF ) +local function lshift(x, by) + return x * 2 ^ by +end + +-- big-endian uint64 of a number +function otp.write_uint64_be(v) + local b1 = bitand( rshift(v, 56), 0xFF ) + local b2 = bitand( rshift(v, 48), 0xFF ) + local b3 = bitand( rshift(v, 40), 0xFF ) + local b4 = bitand( rshift(v, 32), 0xFF ) + local b5 = bitand( rshift(v, 24), 0xFF ) + local b6 = bitand( rshift(v, 16), 0xFF ) + local b7 = bitand( rshift(v, 8), 0xFF ) + local b8 = bitand( rshift(v, 0), 0xFF ) return string.char(b1, b2, b3, b4, b5, b6, b7, b8) end @@ -61,29 +76,49 @@ end function otp.hmac(key, message) local i_key_pad = "" for i=1,64 do - i_key_pad = i_key_pad .. string.char(otp.bitXOR(string.byte(key, i) or 0x00, string.byte(i_pad, i))) + i_key_pad = i_key_pad .. string.char(bitxor(string.byte(key, i) or 0x00, string.byte(i_pad, i))) end + assert(#i_key_pad == 64) local o_key_pad = "" for i=1,64 do - o_key_pad = o_key_pad .. string.char(otp.bitXOR(string.byte(key, i) or 0x00, string.byte(o_pad, i))) + o_key_pad = o_key_pad .. string.char(bitxor(string.byte(key, i) or 0x00, string.byte(o_pad, i))) end + assert(#o_key_pad == 64) -- concat message local first_msg = i_key_pad for i=1,#message do - first_msg = first_msg .. string.byte(message, i) + first_msg = first_msg .. string.char(string.byte(message, i)) end + assert(#first_msg == 64+8) -- hash first message local hash_sum_1 = minetest.sha1(first_msg, true) + assert(#hash_sum_1 == 20) -- concat first message to secons local second_msg = o_key_pad for i=1,#hash_sum_1 do - second_msg = second_msg .. string.byte(hash_sum_1, i) + second_msg = second_msg .. string.char(string.byte(hash_sum_1, i)) end + assert(#second_msg == 64+20) - -- hash final message - return minetest.sha1(second_msg, true) + local hmac = minetest.sha1(second_msg, true) + assert(#hmac == 20) + + return hmac +end + +function otp.generate_code(key, message) + local hmac = otp.hmac(key, message) + + -- https://www.rfc-editor.org/rfc/rfc4226#section-5.4 + local offset = bitand(string.byte(hmac, #hmac), 0xF) + local value = 0 + value = bitor(value, string.byte(hmac, offset+4)) + value = bitor(value, lshift(string.byte(hmac, offset+3), 8)) + value = bitor(value, lshift(string.byte(hmac, offset+2), 16)) + value = bitor(value, lshift(bitand(string.byte(hmac, offset+1), 0x7F), 24)) + return value % 10^6 end \ No newline at end of file diff --git a/functions.spec.lua b/functions.spec.lua new file mode 100644 index 0000000..f53a39d --- /dev/null +++ b/functions.spec.lua @@ -0,0 +1,43 @@ + + +mtt.register("otp.hmac", function(callback) + local secret_b32 = "N6JGKMEKU2E6HQMLLNMJKBRRGVQ2ZKV7" + local secret = otp.basexx.from_base32(secret_b32) + local unix_time = 1640995200 + + local expected_hmac = otp.basexx.from_base64("m04YheEb7i+ThPMUJEfVXVybZFo=") + assert(#expected_hmac == 20) + + local tx = 30 + local ct = math.floor(unix_time / tx) + local counter = otp.write_uint64_be(ct) + + assert( #secret == 20 ) + assert( otp.basexx.to_base64(secret) == "b5JlMIqmiePBi1tYlQYxNWGsqr8=" ) + assert( #counter == 8 ) + assert( otp.basexx.to_base64(counter) == "AAAAAANCp0A=" ) + + local hmac = otp.hmac(secret, counter) + assert(#hmac == 20) + + for i=1,20 do + assert( string.byte(expected_hmac,i) == string.byte(hmac, i) ) + end + + callback() +end) + +mtt.register("otp.generate_code", function(callback) + local expected_code = 699847 + local secret_b32 = "N6JGKMEKU2E6HQMLLNMJKBRRGVQ2ZKV7" + local secret = otp.basexx.from_base32(secret_b32) + local unix_time = 1640995200 + + local tx = 30 + local ct = math.floor(unix_time / tx) + local counter = otp.write_uint64_be(ct) + + local code = otp.generate_code(secret, counter) + assert(code == expected_code) + callback() +end) \ No newline at end of file diff --git a/init.lua b/init.lua index 7501d6a..4fc8ee8 100644 --- a/init.lua +++ b/init.lua @@ -9,4 +9,7 @@ otp = { } dofile(MP.."/functions.lua") -dofile(MP.."/test.lua") + +if minetest.get_modpath("mtt") and mtt.enabled then + dofile(MP.."/functions.spec.lua") +end \ No newline at end of file diff --git a/mod.conf b/mod.conf index ef1fbf1..5772c4e 100644 --- a/mod.conf +++ b/mod.conf @@ -1 +1,2 @@ -name = otp \ No newline at end of file +name = otp +optional_depends = mtt \ No newline at end of file diff --git a/test.lua b/test.lua deleted file mode 100644 index 4808929..0000000 --- a/test.lua +++ /dev/null @@ -1,24 +0,0 @@ -local secret_b32 = "N6JGKMEKU2E6HQMLLNMJKBRRGVQ2ZKV7" -local expected_code = 699847 - -minetest.register_chatcommand("otp_test", { - description = "", - params = "[]", - func = function(name, param) - local secret = otp.basexx.from_base32(secret_b32) - local unix_time = 1640995200 - local tx = 30 - local ct = math.floor(unix_time / tx) - - local hmac = otp.hmac(secret, otp.write_uint64(ct)) - - - - print(dump({ - expected_code = expected_code, - unix_time = unix_time, - ct = ct, - hmac = otp.basexx.to_base64(hmac) - })) - end -}) \ No newline at end of file diff --git a/test/Dockerfile b/test/Dockerfile new file mode 100644 index 0000000..598934d --- /dev/null +++ b/test/Dockerfile @@ -0,0 +1,11 @@ +ARG ENGINE_VERSION=5.6.1 +FROM registry.gitlab.com/minetest/minetest/server:${ENGINE_VERSION} + +USER root + +RUN apk add git &&\ + mkdir -p /root/.minetest/worlds/world/worldmods/ &&\ + cd /root/.minetest/worlds/world/worldmods/ &&\ + git clone https://github.com/BuckarooBanzay/mtt + +ENTRYPOINT minetestserver --config /minetest.conf \ No newline at end of file diff --git a/test/minetest.conf b/test/minetest.conf new file mode 100644 index 0000000..17d0b87 --- /dev/null +++ b/test/minetest.conf @@ -0,0 +1,3 @@ +default_game = minetest_game +mg_name = v7 +mtt_enable = true