From 7f619e46398358a988dcb82aaf93e3e537a750d2 Mon Sep 17 00:00:00 2001 From: BuckarooBanzay Date: Sat, 28 Jan 2023 12:04:14 +0100 Subject: [PATCH] init --- .luacheckrc | 17 +++ basexx.lua | 297 +++++++++++++++++++++++++++++++++++++++++ functions.lua | 89 ++++++++++++ init.lua | 12 ++ license.txt | 25 ++++ mod.conf | 1 + readme.md | 10 ++ test.lua | 24 ++++ workflows/luacheck.yml | 12 ++ 9 files changed, 487 insertions(+) create mode 100644 .luacheckrc create mode 100644 basexx.lua create mode 100644 functions.lua create mode 100644 init.lua create mode 100644 license.txt create mode 100644 mod.conf create mode 100644 readme.md create mode 100644 test.lua create mode 100644 workflows/luacheck.yml diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..f7c631c --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,17 @@ +globals = { + "otp" +} + +read_globals = { + -- Stdlib + string = {fields = {"split", "trim"}}, + table = {fields = {"copy", "getn"}}, + + -- Minetest + "minetest", "vector", "ItemStack", + "dump", "dump2", + "VoxelArea", + + -- testing + "mtt" +} diff --git a/basexx.lua b/basexx.lua new file mode 100644 index 0000000..be5cd53 --- /dev/null +++ b/basexx.lua @@ -0,0 +1,297 @@ +-------------------------------------------------------------------------------- +-- util functions +-------------------------------------------------------------------------------- + +local function divide_string( str, max ) + local result = {} + + local start = 1 + for i = 1, #str do + if i % max == 0 then + table.insert( result, str:sub( start, i ) ) + start = i + 1 + elseif i == #str then + table.insert( result, str:sub( start, i ) ) + end + end + + return result +end + +local function number_to_bit( num, length ) + local bits = {} + + while num > 0 do + local rest = math.floor( math.fmod( num, 2 ) ) + table.insert( bits, rest ) + num = ( num - rest ) / 2 + end + + while #bits < length do + table.insert( bits, "0" ) + end + + return string.reverse( table.concat( bits ) ) +end + +local function ignore_set( str, set ) + if set then + str = str:gsub( "["..set.."]", "" ) + end + return str +end + +local function pure_from_bit( str ) + return ( str:gsub( '........', function ( cc ) + return string.char( tonumber( cc, 2 ) ) + end ) ) +end + +local function unexpected_char_error( str, pos ) + local c = string.sub( str, pos, pos ) + return string.format( "unexpected character at position %d: '%s'", pos, c ) +end + +-------------------------------------------------------------------------------- + +local basexx = {} + +-------------------------------------------------------------------------------- +-- base2(bitfield) decode and encode function +-------------------------------------------------------------------------------- + +local bitMap = { o = "0", i = "1", l = "1" } + +function basexx.from_bit( str, ignore ) + str = ignore_set( str, ignore ) + str = string.lower( str ) + str = str:gsub( '[ilo]', function( c ) return bitMap[ c ] end ) + local pos = string.find( str, "[^01]" ) + if pos then return nil, unexpected_char_error( str, pos ) end + + return pure_from_bit( str ) +end + +function basexx.to_bit( str ) + return ( str:gsub( '.', function ( c ) + local byte = string.byte( c ) + local bits = {} + for _ = 1,8 do + table.insert( bits, byte % 2 ) + byte = math.floor( byte / 2 ) + end + return table.concat( bits ):reverse() + end ) ) +end + +-------------------------------------------------------------------------------- +-- base16(hex) decode and encode function +-------------------------------------------------------------------------------- + +function basexx.from_hex( str, ignore ) + str = ignore_set( str, ignore ) + local pos = string.find( str, "[^%x]" ) + if pos then return nil, unexpected_char_error( str, pos ) end + + return ( str:gsub( '..', function ( cc ) + return string.char( tonumber( cc, 16 ) ) + end ) ) +end + +function basexx.to_hex( str ) + return ( str:gsub( '.', function ( c ) + return string.format('%02X', string.byte( c ) ) + end ) ) +end + +-------------------------------------------------------------------------------- +-- generic function to decode and encode base32/base64 +-------------------------------------------------------------------------------- + +local function from_basexx( str, alphabet, bits ) + local result = {} + for i = 1, #str do + local c = string.sub( str, i, i ) + if c ~= '=' then + local index = string.find( alphabet, c, 1, true ) + if not index then + return nil, unexpected_char_error( str, i ) + end + table.insert( result, number_to_bit( index - 1, bits ) ) + end + end + + local value = table.concat( result ) + local pad = #value % 8 + return pure_from_bit( string.sub( value, 1, #value - pad ) ) +end + +local function to_basexx( str, alphabet, bits, pad ) + local bitString = basexx.to_bit( str ) + + local chunks = divide_string( bitString, bits ) + local result = {} + for _,value in ipairs( chunks ) do + if ( #value < bits ) then + value = value .. string.rep( '0', bits - #value ) + end + local pos = tonumber( value, 2 ) + 1 + table.insert( result, alphabet:sub( pos, pos ) ) + end + + table.insert( result, pad ) + return table.concat( result ) +end + +-------------------------------------------------------------------------------- +-- rfc 3548: http://www.rfc-editor.org/rfc/rfc3548.txt +-------------------------------------------------------------------------------- + +local base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" +local base32PadMap = { "", "======", "====", "===", "=" } + +function basexx.from_base32( str, ignore ) + str = ignore_set( str, ignore ) + return from_basexx( string.upper( str ), base32Alphabet, 5 ) +end + +function basexx.to_base32( str ) + return to_basexx( str, base32Alphabet, 5, base32PadMap[ #str % 5 + 1 ] ) +end + +-------------------------------------------------------------------------------- +-- crockford: http://www.crockford.com/wrmg/base32.html +-------------------------------------------------------------------------------- + +local crockfordAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" +local crockfordMap = { O = "0", I = "1", L = "1" } + +function basexx.from_crockford( str, ignore ) + str = ignore_set( str, ignore ) + str = string.upper( str ) + str = str:gsub( '[ILOU]', function( c ) return crockfordMap[ c ] end ) + return from_basexx( str, crockfordAlphabet, 5 ) +end + +function basexx.to_crockford( str ) + return to_basexx( str, crockfordAlphabet, 5, "" ) +end + +-------------------------------------------------------------------------------- +-- base64 decode and encode function +-------------------------------------------------------------------------------- + +local base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".. + "abcdefghijklmnopqrstuvwxyz".. + "0123456789+/" +local base64PadMap = { "", "==", "=" } + +function basexx.from_base64( str, ignore ) + str = ignore_set( str, ignore ) + return from_basexx( str, base64Alphabet, 6 ) +end + +function basexx.to_base64( str ) + return to_basexx( str, base64Alphabet, 6, base64PadMap[ #str % 3 + 1 ] ) +end + +-------------------------------------------------------------------------------- +-- URL safe base64 decode and encode function +-------------------------------------------------------------------------------- + +local url64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".. + "abcdefghijklmnopqrstuvwxyz".. + "0123456789-_" + +function basexx.from_url64( str, ignore ) + str = ignore_set( str, ignore ) + return from_basexx( str, url64Alphabet, 6 ) +end + +function basexx.to_url64( str ) + return to_basexx( str, url64Alphabet, 6, "" ) +end + +-------------------------------------------------------------------------------- +-- +-------------------------------------------------------------------------------- + +local function length_error( len, d ) + return string.format( "invalid length: %d - must be a multiple of %d", len, d ) +end + +local z85Decoder = { 0x00, 0x44, 0x00, 0x54, 0x53, 0x52, 0x48, 0x00, + 0x4B, 0x4C, 0x46, 0x41, 0x00, 0x3F, 0x3E, 0x45, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x40, 0x00, 0x49, 0x42, 0x4A, 0x47, + 0x51, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, + 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, + 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, + 0x3B, 0x3C, 0x3D, 0x4D, 0x00, 0x4E, 0x43, 0x00, + 0x00, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, + 0x21, 0x22, 0x23, 0x4F, 0x00, 0x50, 0x00, 0x00 } + +function basexx.from_z85( str, ignore ) + str = ignore_set( str, ignore ) + if ( #str % 5 ) ~= 0 then + return nil, length_error( #str, 5 ) + end + + local result = {} + + local value = 0 + for i = 1, #str do + local index = string.byte( str, i ) - 31 + if index < 1 or index >= #z85Decoder then + return nil, unexpected_char_error( str, i ) + end + value = ( value * 85 ) + z85Decoder[ index ] + if ( i % 5 ) == 0 then + local divisor = 256 * 256 * 256 + while divisor ~= 0 do + local b = math.floor( value / divisor ) % 256 + table.insert( result, string.char( b ) ) + divisor = math.floor( divisor / 256 ) + end + value = 0 + end + end + + return table.concat( result ) +end + +local z85Encoder = "0123456789".. + "abcdefghijklmnopqrstuvwxyz".. + "ABCDEFGHIJKLMNOPQRSTUVWXYZ".. + ".-:+=^!/*?&<>()[]{}@%$#" + +function basexx.to_z85( str ) + if ( #str % 4 ) ~= 0 then + return nil, length_error( #str, 4 ) + end + + local result = {} + + local value = 0 + for i = 1, #str do + local b = string.byte( str, i ) + value = ( value * 256 ) + b + if ( i % 4 ) == 0 then + local divisor = 85 * 85 * 85 * 85 + while divisor ~= 0 do + local index = ( math.floor( value / divisor ) % 85 ) + 1 + table.insert( result, z85Encoder:sub( index, index ) ) + divisor = math.floor( divisor / 85 ) + end + value = 0 + end + end + + return table.concat( result ) +end + +-------------------------------------------------------------------------------- + +return basexx diff --git a/functions.lua b/functions.lua new file mode 100644 index 0000000..d83dd53 --- /dev/null +++ b/functions.lua @@ -0,0 +1,89 @@ + +-- https://stackoverflow.com/a/25594410 +function otp.bitXOR(a,b)--Bitwise xor + local p,c=1,0 + while a>0 and b>0 do + local ra,rb=a%2,b%2 + if ra~=rb then c=c+p end + a,b,p=(a-ra)/2,(b-rb)/2,p*2 + end + if a0 do + local ra=a%2 + if ra>0 then c=c+p end + a,p=(a-ra)/2,p*2 + end + return c +end + +-- https://stackoverflow.com/a/32387452 +local function bitand(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + if a % 2 == 1 and b % 2 == 1 then -- test the rightmost bits + result = result + bitval -- set the current bit + end + bitval = bitval * 2 -- shift left + a = math.floor(a/2) -- shift right + b = math.floor(b/2) + end + return result +end + +-- https://gist.github.com/mebens/938502 +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 ) + return string.char(b1, b2, b3, b4, b5, b6, b7, b8) +end + +-- prepare paddings +-- https://en.wikipedia.org/wiki/HMAC +local i_pad = "" +local o_pad = "" +for _=1,64 do + i_pad = i_pad .. string.char(0x36) + o_pad = o_pad .. string.char(0x5c) +end + +-- hmac generation +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))) + end + + 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))) + end + + -- concat message + local first_msg = i_key_pad + for i=1,#message do + first_msg = first_msg .. string.byte(message, i) + end + + -- hash first message + local hash_sum_1 = minetest.sha1(first_msg, true) + + -- 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) + end + + -- hash final message + return minetest.sha1(second_msg, true) +end \ No newline at end of file diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..7501d6a --- /dev/null +++ b/init.lua @@ -0,0 +1,12 @@ +local MP = minetest.get_modpath("otp") + +otp = { + -- mod storage + storage = minetest.get_mod_storage(), + + -- baseXX functions + basexx = loadfile(MP.."/basexx.lua")() +} + +dofile(MP.."/functions.lua") +dofile(MP.."/test.lua") diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..cd38198 --- /dev/null +++ b/license.txt @@ -0,0 +1,25 @@ +License of source code +---------------------- + +The MIT License (MIT) +Copyright (C) 2023 BuckarooBanzay + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +For more details: +https://opensource.org/licenses/MIT + diff --git a/mod.conf b/mod.conf new file mode 100644 index 0000000..ef1fbf1 --- /dev/null +++ b/mod.conf @@ -0,0 +1 @@ +name = otp \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..cd63d1c --- /dev/null +++ b/readme.md @@ -0,0 +1,10 @@ + +# (T)OTP mod for minetest + +* State: **WIP** + + +# License + +* Code: `MIT` +* "basexx.lua" `MIT` https://github.com/aiq/basexx/blob/master/lib/basexx.lua \ No newline at end of file diff --git a/test.lua b/test.lua new file mode 100644 index 0000000..4808929 --- /dev/null +++ b/test.lua @@ -0,0 +1,24 @@ +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/workflows/luacheck.yml b/workflows/luacheck.yml new file mode 100644 index 0000000..c4385ea --- /dev/null +++ b/workflows/luacheck.yml @@ -0,0 +1,12 @@ + +on: [push, pull_request] +name: luacheck +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: lint + uses: Roang-zero1/factorio-mod-luacheck@master + with: + luacheckrc_url: "" \ No newline at end of file