diff --git a/.github/workflows/luacheck.yml b/.github/workflows/luacheck.yml deleted file mode 100644 index c4385ea..0000000 --- a/.github/workflows/luacheck.yml +++ /dev/null @@ -1,12 +0,0 @@ - -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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 2a0cc88..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,20 +0,0 @@ -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 test diff --git a/.luacheckrc b/.luacheckrc index 88f3d9b..35bb0d5 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,5 +1,5 @@ globals = { - "otp", + "fediauth", "minetest" } @@ -13,10 +13,5 @@ read_globals = { "dump", "dump2", "VoxelArea", - -- testing - "mtt" } -files["qrencode.lua"] = { - ignore = {"631"} -} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 8813ac1..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,22 +0,0 @@ -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" - - minetest: - image: registry.gitlab.com/minetest/minetest/server:5.6.1 - user: root - ports: - - "30000:30000/udp" - volumes: - - "./:/root/.minetest/worlds/world/worldmods/otp/" - - "world:/root/.minetest/worlds/world" - -volumes: - world: {} \ No newline at end of file diff --git a/functions.lua b/functions.lua index f38edfb..dffaffb 100644 --- a/functions.lua +++ b/functions.lua @@ -51,7 +51,7 @@ local function lshift(x, by) end -- big-endian uint64 of a number -function otp.write_uint64_be(v) +function fediauth.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 ) @@ -73,7 +73,7 @@ for _=1,64 do end -- hmac generation -function otp.hmac(key, message) +function fediauth.hmac(key, message) local i_key_pad = "" for i=1,64 do i_key_pad = i_key_pad .. string.char(bitxor(string.byte(key, i) or 0x00, string.byte(i_pad, i))) @@ -117,16 +117,16 @@ local function left_pad(str, s, len) return str end -function otp.generate_totp(secret_b32, unix_time) - local key = otp.basexx.from_base32(secret_b32) +function fediauth.generate_tfediauth(secret_b32, unix_time) + local key = fediauth.basexx.from_base32(secret_b32) unix_time = unix_time or os.time() local tx = 30 local ct = math.floor(unix_time / tx) - local counter = otp.write_uint64_be(ct) + local counter = fediauth.write_uint64_be(ct) local valid_seconds = ((ct * tx) + tx) - unix_time - local hmac = otp.hmac(key, counter) + local hmac = fediauth.hmac(key, counter) -- https://www.rfc-editor.org/rfc/rfc4226#section-5.4 local offset = bitand(string.byte(hmac, #hmac), 0xF) @@ -141,7 +141,7 @@ function otp.generate_totp(secret_b32, unix_time) return padded_code, valid_seconds end -function otp.create_qr_png(data) +function fediauth.create_qr_png(data) local height = #data + 2 local width = height @@ -174,7 +174,7 @@ function otp.create_qr_png(data) return minetest.encode_png(width, height, png_data, 2) end -function otp.generate_secret() +function fediauth.generate_secret() local buf = minetest.sha1("" .. math.random(10000), true) local s = "" for i=1,20 do @@ -184,30 +184,46 @@ function otp.generate_secret() end -- get or generate per-player secret b32 ecoded -function otp.get_player_secret_b32(name) - local secret_b32 = otp.storage:get_string(name .. "_secret") +function fediauth.get_player_secret_b32(name) + local secret_b32 = fediauth.storage:get_string(name .. "_secret") if secret_b32 == "" then - secret_b32 = otp.basexx.to_base32(otp.generate_secret()) - otp.storage:set_string(name .. "_secret", secret_b32) + secret_b32 = fediauth.basexx.to_base32(fediauth.generate_secret()) + fediauth.storage:set_string(name .. "_secret", secret_b32) end return secret_b32 end --- returns true if the player is otp enabled _and_ set up properly -function otp.is_player_enabled(name) - local has_secret = otp.storage:get_string(name .. "_secret") ~= "" - local has_priv = minetest.check_player_privs(name, "otp_enabled") +-- returns true if the player is fediauth enabled _and_ set up properly +function fediauth.is_player_enabled(name) + local has_secret = fediauth.storage:get_string(name .. "_secret") ~= "" + local has_priv = minetest.check_player_privs(name, "fediauth_enabled") return has_secret and has_priv end -function otp.check_code(secret_b32, code, time) +function fediauth.is_player_bypassed(name) + local has_priv = minetest.check_player_privs(name, "fediauth_bypass") + + return has_priv +end + +function fediauth.check_code(secret_b32, code, time) time = time or os.time() - for _, t_offset in ipairs({0, -30, 30}) do - local expected_code = otp.generate_totp(secret_b32, time + t_offset) + for _, t_offset in ipairs({0, -30, 30, -60, 60}) do + local expected_code = fediauth.generate_tfediauth(secret_b32, time + t_offset) if expected_code == code then return true end end return false -end \ No newline at end of file +end + +function fediauth.give_code(secret_b32, time) + time = time or os.time() + local codeseq = {} + for _, t_offset in ipairs({60, 30, 0, -30, -60}) do + local expected_code = fediauth.generate_tfediauth(secret_b32, time + t_offset) + table.insert(codeseq, expected_code) + end + return codeseq +end diff --git a/functions.spec.lua b/functions.spec.lua deleted file mode 100644 index 57d7013..0000000 --- a/functions.spec.lua +++ /dev/null @@ -1,80 +0,0 @@ - - -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_totp", function(callback) - local expected_code = "699847" - local secret_b32 = "N6JGKMEKU2E6HQMLLNMJKBRRGVQ2ZKV7" - local unix_time = 1640995200 - - local code, valid_seconds = otp.generate_totp(secret_b32, unix_time) - assert(code == expected_code) - assert(valid_seconds > 0) - - code, valid_seconds = otp.generate_totp(secret_b32) - print("Current code: " .. code .. " valid for " .. valid_seconds .. " seconds") - callback() -end) - -mtt.register("otp.check_code", function(callback) - local expected_code = "699847" - local secret_b32 = "N6JGKMEKU2E6HQMLLNMJKBRRGVQ2ZKV7" - local unix_time = 1640995200 - - assert(otp.check_code(secret_b32, expected_code, unix_time)) - assert(otp.check_code(secret_b32, expected_code, unix_time+30)) - assert(otp.check_code(secret_b32, expected_code, unix_time-30)) - assert(not otp.check_code(secret_b32, expected_code, unix_time-60)) - assert(not otp.check_code(secret_b32, expected_code, unix_time+60)) - assert(not otp.check_code(secret_b32, expected_code)) - - callback() -end) - -mtt.register("otp.create_qr_png", function(callback) - local url = "otpauth://totp/abc:myaccount?algorithm=SHA1&digits=6&issuer=abc&period=30&" - .. "secret=N6JGKMEKU2E6HQMLLNMJKBRRGVQ2ZKV7" - - local ok, code = otp.qrcode(url) - assert(ok) - assert(code) - - local png = otp.create_qr_png(code) - assert(png) - - local f = io.open(minetest.get_worldpath() .. "/qr.png", "w") - f:write(png) - f:close() - callback() -end) - -mtt.register("otp.generate_secret", function(callback) - local s = otp.generate_secret() - assert(#s == 20) - callback() -end) \ No newline at end of file diff --git a/init.lua b/init.lua index 72f4061..92d82b5 100644 --- a/init.lua +++ b/init.lua @@ -1,25 +1,28 @@ -local MP = minetest.get_modpath("otp") +local MP = minetest.get_modpath("fediauth") +local http = minetest.request_http_api() -- sanity checks assert(type(minetest.encode_png) == "function") -otp = { +fediauth = { -- mod storage storage = minetest.get_mod_storage(), -- baseXX functions basexx = loadfile(MP.."/basexx.lua")(), - - -- qr code - qrcode = loadfile(MP.."/qrencode.lua")().qrcode } -dofile(MP.."/functions.lua") -dofile(MP.."/onboard.lua") -dofile(MP.."/join.lua") -dofile(MP.."/privs.lua") -dofile(MP.."/priv_revoke.lua") - -if minetest.get_modpath("mtt") and mtt.enabled then - dofile(MP.."/functions.spec.lua") +dofile(MP.."/mastoapi.lua") +local instance = minetest.settings:get("fediauth.instance") +local key = minetest.settings:get("fediauth.api_token") +if not instance or not key then + minetest.log("warning", "[fediauth] For working fediauth you should specify fediauth.instance and fediauth.api_token") +else + mastoapi_init(http, instance, key) + dofile(MP.."/functions.lua") + dofile(MP.."/onboard.lua") + dofile(MP.."/join.lua") + dofile(MP.."/privs.lua") + dofile(MP.."/priv_revoke.lua") end + diff --git a/join.lua b/join.lua index 305466b..fbedf40 100644 --- a/join.lua +++ b/join.lua @@ -1,26 +1,73 @@ -local FORMNAME = "otp-check" +local FORMNAME = "fediauth-check" +local FORMNAMEFEDI = "fediauth-check-fedi" --- time for otp code verification -local otp_time = 300 +-- time for fediauth code verification +local fediauth_time = 300 -- playername => start_time -local otp_sessions = {} +local fediauth_sessions = {} --- Code formspec on join for otp enabled players +local formspecfediadd = "size[9,10]" .. + "label[1,7;Input your fediverse account handle]" .. + "image[1.5,0.6;7,7;fediverse.png]" .. + "field[1,9;4,1;fediverse_account_url;@nick@example.com;]" .. + "button[5,8.7;3,1;submit;Send code]" + +local feditempstore = {} + +minetest.register_entity("fediauth:checkmark", { + initial_properties = { + pointable = false, + armor_groups = { immortal = 1 }, + visual = "sprite", + visual_size = {x = 0.5, y = 0.5}, + textures = { "checkmark.png^[opacity:180" }, + use_texture_alpha = true, + static_save = false, + glow = 5, + }, + on_detach = function(self, parent) self.object:remove() end +}) + +function fediauth.verified_checkmark(player, verified) + local tag = player:get_player_name() + local props = player:get_properties() + if verified then + local obj = minetest.add_entity({x=0,y=2,z=0}, "fediauth:checkmark", nil) + obj:set_attach(player, "Head", {x = 0, y = 12, z = 0}) + player:set_properties({nametag = props.nametag .. " [FEDI]", nametag_color = "#00ff00" }) + end +end + +-- Code formspec on join for fediauth enabled players minetest.register_on_joinplayer(function(player) local playername = player:get_player_name() - if otp.is_player_enabled(playername) then - minetest.log("action", "[otp] session start for player: '" .. playername .. "'") + if fediauth.is_player_bypassed(playername) then return end + if fediauth.is_player_enabled(playername) or minetest.settings:get_bool("fediauth.fedi_required", false) then + minetest.log("action", "[fediauth] session start for player: '" .. playername .. "'") - -- start otp session time - otp_sessions[player:get_player_name()] = os.time() + -- start fediauth session time + fediauth_sessions[player:get_player_name()] = os.time() -- revoke important privs and re-grant again on code-verification - otp.revoke_privs(playername) + fediauth.revoke_privs(playername) + -- if fedi only allowed + if minetest.settings:get_bool("fediauth.fedi_required", false) then + local existsfedi = fediauth.storage:get_string(playername .. "_fedi") + if existsfedi == "" or not existsfedi then + minetest.log("action", "[fediauth] request fedi account for player: '" .. playername .. "'") + minetest.show_formspec(playername, FORMNAMEFEDI, formspecfediadd) + return + end + end + + local secret_b32 = fediauth.get_player_secret_b32(playername) + local codeseq = fediauth.give_code(secret_b32) + fediauth.send_code(codeseq[1], fediauth.storage:get_string(playername .. "_fedi")) -- send verification formspec local formspec = "size[10,2]" .. - "label[1,0;Please enter your OTP code below]" .. + "label[1,0;Please check your fedi account and enter code]" .. "field[1,1.3;4,1;code;Code;]" .. "button_exit[5,1;3,1;submit;Verify]" @@ -28,39 +75,73 @@ minetest.register_on_joinplayer(function(player) end end) --- clear otp session on leave +-- clear fediauth session on leave minetest.register_on_leaveplayer(function(player) local playername = player:get_player_name() - otp_sessions[playername] = nil + fediauth_sessions[playername] = nil end) -- check sessions periodically and kick if timed out local function session_check() local now = os.time() - for name, start_time in pairs(otp_sessions) do - if (now - start_time) > otp_time then - minetest.kick_player(name, "OTP Code validation timed out") - otp_sessions[name] = nil + for name, start_time in pairs(fediauth_sessions) do + if (now - start_time) > fediauth_time then + minetest.kick_player(name, "fediauth code validation timed out") + fediauth_sessions[name] = nil end end minetest.after(5, session_check) end minetest.after(5, session_check) --- otp check +-- fediauth check minetest.register_on_player_receive_fields(function(player, formname, fields) - if formname ~= FORMNAME then + if formname ~= FORMNAME and formname ~= FORMNAMEFEDI then return end local playername = player:get_player_name() - local secret_b32 = otp.get_player_secret_b32(playername) - if otp.check_code(secret_b32, fields.code) then - minetest.chat_send_player(playername, "OTP Code validation succeeded") - otp_sessions[playername] = nil - otp.regrant_privs(playername) - else - minetest.kick_player(playername, "OTP Code validation failed") - otp.regrant_privs(playername) + local secret_b32 = fediauth.get_player_secret_b32(playername) + + -- check for new player or doesn't have fedi account + if fields.fediverse_account_url then + if not string.starts(fields.fediverse_account_url, "@") or string.len(fields.fediverse_account_url) < 3 or string.len(fields.fediverse_account_url) > 100 then + minetest.chat_send_player(playername, minetest.colorize("#ff0000", "Try again, your input is incorrect")) + minetest.show_formspec(playername, FORMNAMEFEDI, formspecfediadd) + return + end + local secret_b32 = fediauth.get_player_secret_b32(playername) + local codeseq = fediauth.give_code(secret_b32) + fediauth.send_code(codeseq[1], fields.fediverse_account_url) + feditempstore[playername] = fields.fediverse_account_url + local formspec = "size[9,10]" .. + "label[1,7;Check code on " .. minetest.formspec_escape(fields.fediverse_account_url) .. "]" .. + "field[1,9;4,1;code;Code;]" .. + "button_exit[5,8.7;3,1;submit;Verify]" + + minetest.show_formspec(playername, FORMNAME, formspec) + return end -end) \ No newline at end of file + + + + + if fediauth.check_code(secret_b32, fields.code) then + local fedi_account = fediauth.storage:get_string(playername .. "_fedi") + + -- for account without fediverse (for prevent write account if code incorrect + if fedi_account == "" and feditempstore[playername] then + fediauth.storage:set_string(playername .. "_fedi", feditempstore[playername]) + fedi_account = feditempstore[playername] + feditempstore[playername] = nil + end + + minetest.chat_send_player(playername, minetest.colorize("#00ff00", "fediauth code validation succeeded for " .. fedi_account)) + fediauth_sessions[playername] = nil + fediauth.regrant_privs(playername) + fediauth.verified_checkmark(player, true) + else + minetest.kick_player(playername, "fediauth code validation failed") + fediauth.regrant_privs(playername) + end +end) diff --git a/license.txt b/license.txt index cd38198..6ca207e 100644 --- a/license.txt +++ b/license.txt @@ -1,25 +1,122 @@ -License of source code ----------------------- +Creative Commons Legal Code -The MIT License (MIT) -Copyright (C) 2023 BuckarooBanzay +CC0 1.0 Universal -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: + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. +Statement of Purpose -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. +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). -For more details: -https://opensource.org/licenses/MIT +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/mastoapi.lua b/mastoapi.lua new file mode 100644 index 0000000..7e37b79 --- /dev/null +++ b/mastoapi.lua @@ -0,0 +1,27 @@ +local http, instance, key + +function fediauth.send_code(code, account_handle) + local status = { + visibility = "direct", + status = "" .. code .. " " .. account_handle .. " code for minetest fediauth, do not share it!" + } + local json = minetest.write_json(status) + http.fetch({ + url = "https://" .. instance .. "/api/v1/statuses", + extra_headers = { "Content-Type: application/json", "Authorization: Bearer " .. key }, + timeout = 15, + post_data = json + }, function(res) + if res then + minetest.log("action", "[fediauth] code sent to: '" .. account_handle .. "'") + else + minetest.log("error", "[fediauth] code not sent to: '" .. account_handle .. "'") + end + end) +end + +function mastoapi_init(h, i, k) + http = h + instance = i + key = k +end diff --git a/mod.conf b/mod.conf index d63c060..52ad125 100644 --- a/mod.conf +++ b/mod.conf @@ -1,3 +1,2 @@ -name = otp -optional_depends = mtt -min_minetest_version = 5.3 \ No newline at end of file +name = fediauth +min_minetest_version = 5.3 diff --git a/onboard.lua b/onboard.lua index 6645c3c..d84ab1f 100644 --- a/onboard.lua +++ b/onboard.lua @@ -1,78 +1,81 @@ -local FORMNAME = "otp-onboard" +local FORMNAME = "fediauth-onboard" +local FORMNAMEFEDI = "fediauth-onboard-fedi" -minetest.register_chatcommand("otp_disable", { - description = "Disable the otp verification", - privs = { otp_enabled = true, interact = true }, +local feditempstore = {} + +function string.starts(String,Start) + return string.sub(String,1,string.len(Start))==Start +end + +minetest.register_chatcommand("fediauth_off", { + description = "Disable the fediauth verification", + privs = { fediauth_enabled = true, interact = true }, func = function(name) -- clear priv local privs = minetest.get_player_privs(name) - privs.otp_enabled = nil + privs.fediauth_enabled = nil minetest.set_player_privs(name, privs) - return true, "OTP login disabled" + return true, "fediauth login disabled" end }) -minetest.register_chatcommand("otp_enable", { - description = "Enable the otp verification", +minetest.register_chatcommand("fediauth_on", { + description = "Enable the fediauth verification", func = function(name) - -- issuer name - local issuer = minetest.settings:get("otp.issuer") or "Minetest" - local server_name = minetest.settings:get("server_name") - local server_address = minetest.settings:get("server_address") - if server_name and server_name ~= "" then - issuer = server_name - elseif server_address and server_address ~= "" then - issuer = server_address + local secret_b32 = fediauth.get_player_secret_b32(name) + + local formspec_account = "size[9,10]" .. + "label[1,7;Input your fediverse account handle]" .. + "image[1.5,0.6;7,7;fediverse.png]" .. + "field[1,9;4,1;fediverse_account_url;@nick@example.com;]" .. + "button[5,8.7;3,1;submit;Send code]" + + minetest.show_formspec(name, FORMNAMEFEDI, formspec_account) end - - -- authenticator image - local image = minetest.settings:get("otp.authenticator_image") or - "https://raw.githubusercontent.com/minetest/minetest/master/misc/minetest-xorg-icon-128.png" - - local secret_b32 = otp.get_player_secret_b32(name) - - -- url for the qr code - local url = "otpauth://totp/" .. issuer .. ":" .. name .. "?algorithm=SHA1" .. - "&digits=6" .. - "&issuer=" .. issuer .. - "&period=30" .. - "&secret=" .. secret_b32 .. - "&image=" .. image - - local ok, code = otp.qrcode(url) - if not ok then - return false, "qr code generation failed" - end - - local png = otp.create_qr_png(code) - local formspec = "size[9,10]" .. - "image[1.5,0.6;7,7;^[png:" .. minetest.encode_base64(png) .. "]" .. - "label[1,7;Use the above QR code in your OTP-App to obtain a verification code]" .. - "field[1,9;4,1;code;Code;]" .. - "button_exit[5,8.7;3,1;submit;Verify]" - - minetest.show_formspec(name, FORMNAME, formspec) - end }) minetest.register_on_player_receive_fields(function(player, formname, fields) - if formname ~= FORMNAME then + if formname ~= FORMNAME and formname ~= FORMNAMEFEDI then return end + if fields.fediverse_account_url then + local playername = player:get_player_name() + if not string.starts(fields.fediverse_account_url, "@") or string.len(fields.fediverse_account_url) < 3 or string.len(fields.fediverse_account_url) > 100 then + minetest.chat_send_player(playername, minetest.colorize("#ff0000", "Try again, your input is incorrect")) + return + end + local secret_b32 = fediauth.get_player_secret_b32(playername) + local codeseq = fediauth.give_code(secret_b32) + fediauth.send_code(codeseq[1], fields.fediverse_account_url) + feditempstore[playername] = fields.fediverse_account_url + local formspec = "size[9,10]" .. + "label[1,7;Check code on " .. minetest.formspec_escape(fields.fediverse_account_url) .. "]" .. + "field[1,9;4,1;code;Code;]" .. + "button_exit[5,8.7;3,1;submit;Verify]" + + minetest.show_formspec(playername, FORMNAME, formspec) + + end + if fields.code then local playername = player:get_player_name() - local secret_b32 = otp.get_player_secret_b32(playername) - if otp.check_code(secret_b32, fields.code) then + local secret_b32 = fediauth.get_player_secret_b32(playername) + if fediauth.check_code(secret_b32, fields.code) then -- set priv local privs = minetest.get_player_privs(playername) - privs.otp_enabled = true + privs.fediauth_enabled = true minetest.set_player_privs(playername, privs) + fediauth.verified_checkmark(player, true) + if feditempstore[playername] then + fediauth.storage:set_string(playername .. "_fedi", feditempstore[playername]) + feditempstore[playername] = nil + end - minetest.chat_send_player(playername, "Code validation succeeded, OTP login enabled") + minetest.chat_send_player(playername, "Code validation succeeded, fediauth login enabled") else minetest.chat_send_player(playername, "Code validation failed!") end end -end) \ No newline at end of file +end) diff --git a/priv_revoke.lua b/priv_revoke.lua index 0659013..4c68289 100644 --- a/priv_revoke.lua +++ b/priv_revoke.lua @@ -2,18 +2,18 @@ -- privs to revoke until the verification code is validated local temp_revoke_privs = {} --- mark player health-related privs as "otp_keep" (they don't get removed while entering the otp code) +-- mark player health-related privs as "fediauth_keep" (they don't get removed while entering the fediauth code) for _, name in ipairs({"fly", "noclip"}) do local priv_def = minetest.registered_privileges[name] if priv_def then - priv_def.otp_keep = true + priv_def.fediauth_keep = true end end minetest.register_on_mods_loaded(function() - -- collect all privs to revoke while entering the otp code + -- collect all privs to revoke while entering the fediauth code for name, priv_def in pairs(minetest.registered_privileges) do - if not priv_def.otp_keep then + if not priv_def.fediauth_keep then -- not marked explicitly as "keep" table.insert(temp_revoke_privs, name) end @@ -21,9 +21,9 @@ minetest.register_on_mods_loaded(function() end) -- moves all "temp_revoke_privs" to mod-storage -function otp.revoke_privs(playername) +function fediauth.revoke_privs(playername) local privs = minetest.get_player_privs(playername) - if otp.storage:get_string(playername .. "_privs") == "" then + if fediauth.storage:get_string(playername .. "_privs") == "" then local moved_privs = {} for _, priv_name in ipairs(temp_revoke_privs) do @@ -33,15 +33,15 @@ function otp.revoke_privs(playername) end end - minetest.log("action", "[otp] revoking privs of '" .. playername .. "' list: " .. dump(moved_privs)) + minetest.log("action", "[fediauth] revoking privs of '" .. playername .. "' list: " .. dump(moved_privs)) minetest.set_player_privs(playername, privs) - otp.storage:set_string(playername .. "_privs", minetest.serialize(moved_privs)) + fediauth.storage:set_string(playername .. "_privs", minetest.serialize(moved_privs)) end end -- moves all privs from mod-storage into the live privs -function otp.regrant_privs(playername) - local stored_priv_str = otp.storage:get_string(playername .. "_privs") +function fediauth.regrant_privs(playername) + local stored_priv_str = fediauth.storage:get_string(playername .. "_privs") if stored_priv_str ~= "" then local privs = minetest.get_player_privs(playername) local stored_privs = minetest.deserialize(stored_priv_str) @@ -51,8 +51,8 @@ function otp.regrant_privs(playername) privs[priv_name] = true end - minetest.log("action", "[otp] regranting privs of '" .. playername .. "' list: " .. dump(stored_privs)) + minetest.log("action", "[fediauth] regranting privs of '" .. playername .. "' list: " .. dump(stored_privs)) minetest.set_player_privs(playername, privs) - otp.storage:set_string(playername .. "_privs", "") + fediauth.storage:set_string(playername .. "_privs", "") end end \ No newline at end of file diff --git a/privs.lua b/privs.lua index ab02a60..4dfd90b 100644 --- a/privs.lua +++ b/privs.lua @@ -1,6 +1,12 @@ -minetest.register_privilege("otp_enabled", { - description = "otp enabled player", +minetest.register_privilege("fediauth_enabled", { + description = "fediauth enabled player", give_to_singleplayer = false, - otp_keep = true + fediauth_keep = true +}) + +minetest.register_privilege("fediauth_bypass", { + description = "fediauth bypass for players who not want type code on each log in (when enabled fediauth.fedi_required)", + give_to_singleplayer = false, + fediauth_keep = true }) diff --git a/qrencode.lua b/qrencode.lua deleted file mode 100644 index c21000b..0000000 --- a/qrencode.lua +++ /dev/null @@ -1,1325 +0,0 @@ ---- The qrcode library is licensed under the 3-clause BSD license (aka "new BSD") ---- To get in contact with the author, mail to . ---- ---- Please report bugs on the [github project page](http://speedata.github.io/luaqrcode/). --- Copyright (c) 2012-2020, Patrick Gundlach and contributors, see https://github.com/speedata/luaqrcode --- All rights reserved. --- --- Redistribution and use in source and binary forms, with or without --- modification, are permitted provided that the following conditions are met: --- * Redistributions of source code must retain the above copyright --- notice, this list of conditions and the following disclaimer. --- * Redistributions in binary form must reproduce the above copyright --- notice, this list of conditions and the following disclaimer in the --- documentation and/or other materials provided with the distribution. --- * Neither the name of SPEEDATA nor the --- names of its contributors may be used to endorse or promote products --- derived from this software without specific prior written permission. --- --- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND --- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED --- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE --- DISCLAIMED. IN NO EVENT SHALL SPEEDATA GMBH BE LIABLE FOR ANY --- DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES --- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; --- LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND --- ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT --- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS --- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - ---- Overall workflow ---- ================ ---- The steps to generate the qrcode, assuming we already have the codeword: ---- ---- 1. Determine version, ec level and mode (=encoding) for codeword ---- 1. Encode data ---- 1. Arrange data and calculate error correction code ---- 1. Generate 8 matrices with different masks and calculate the penalty ---- 1. Return qrcode with least penalty ---- ---- Each step is of course more or less complex and needs further description - ---- Helper functions ---- ================ ---- ---- We start with some helper functions - --- To calculate xor we need to do that bitwise. This helper table speeds up the num-to-bit --- part a bit (no pun intended) -local cclxvi = {[0] = {0,0,0,0,0,0,0,0}, {1,0,0,0,0,0,0,0}, {0,1,0,0,0,0,0,0}, {1,1,0,0,0,0,0,0}, -{0,0,1,0,0,0,0,0}, {1,0,1,0,0,0,0,0}, {0,1,1,0,0,0,0,0}, {1,1,1,0,0,0,0,0}, -{0,0,0,1,0,0,0,0}, {1,0,0,1,0,0,0,0}, {0,1,0,1,0,0,0,0}, {1,1,0,1,0,0,0,0}, -{0,0,1,1,0,0,0,0}, {1,0,1,1,0,0,0,0}, {0,1,1,1,0,0,0,0}, {1,1,1,1,0,0,0,0}, -{0,0,0,0,1,0,0,0}, {1,0,0,0,1,0,0,0}, {0,1,0,0,1,0,0,0}, {1,1,0,0,1,0,0,0}, -{0,0,1,0,1,0,0,0}, {1,0,1,0,1,0,0,0}, {0,1,1,0,1,0,0,0}, {1,1,1,0,1,0,0,0}, -{0,0,0,1,1,0,0,0}, {1,0,0,1,1,0,0,0}, {0,1,0,1,1,0,0,0}, {1,1,0,1,1,0,0,0}, -{0,0,1,1,1,0,0,0}, {1,0,1,1,1,0,0,0}, {0,1,1,1,1,0,0,0}, {1,1,1,1,1,0,0,0}, -{0,0,0,0,0,1,0,0}, {1,0,0,0,0,1,0,0}, {0,1,0,0,0,1,0,0}, {1,1,0,0,0,1,0,0}, -{0,0,1,0,0,1,0,0}, {1,0,1,0,0,1,0,0}, {0,1,1,0,0,1,0,0}, {1,1,1,0,0,1,0,0}, -{0,0,0,1,0,1,0,0}, {1,0,0,1,0,1,0,0}, {0,1,0,1,0,1,0,0}, {1,1,0,1,0,1,0,0}, -{0,0,1,1,0,1,0,0}, {1,0,1,1,0,1,0,0}, {0,1,1,1,0,1,0,0}, {1,1,1,1,0,1,0,0}, -{0,0,0,0,1,1,0,0}, {1,0,0,0,1,1,0,0}, {0,1,0,0,1,1,0,0}, {1,1,0,0,1,1,0,0}, -{0,0,1,0,1,1,0,0}, {1,0,1,0,1,1,0,0}, {0,1,1,0,1,1,0,0}, {1,1,1,0,1,1,0,0}, -{0,0,0,1,1,1,0,0}, {1,0,0,1,1,1,0,0}, {0,1,0,1,1,1,0,0}, {1,1,0,1,1,1,0,0}, -{0,0,1,1,1,1,0,0}, {1,0,1,1,1,1,0,0}, {0,1,1,1,1,1,0,0}, {1,1,1,1,1,1,0,0}, -{0,0,0,0,0,0,1,0}, {1,0,0,0,0,0,1,0}, {0,1,0,0,0,0,1,0}, {1,1,0,0,0,0,1,0}, -{0,0,1,0,0,0,1,0}, {1,0,1,0,0,0,1,0}, {0,1,1,0,0,0,1,0}, {1,1,1,0,0,0,1,0}, -{0,0,0,1,0,0,1,0}, {1,0,0,1,0,0,1,0}, {0,1,0,1,0,0,1,0}, {1,1,0,1,0,0,1,0}, -{0,0,1,1,0,0,1,0}, {1,0,1,1,0,0,1,0}, {0,1,1,1,0,0,1,0}, {1,1,1,1,0,0,1,0}, -{0,0,0,0,1,0,1,0}, {1,0,0,0,1,0,1,0}, {0,1,0,0,1,0,1,0}, {1,1,0,0,1,0,1,0}, -{0,0,1,0,1,0,1,0}, {1,0,1,0,1,0,1,0}, {0,1,1,0,1,0,1,0}, {1,1,1,0,1,0,1,0}, -{0,0,0,1,1,0,1,0}, {1,0,0,1,1,0,1,0}, {0,1,0,1,1,0,1,0}, {1,1,0,1,1,0,1,0}, -{0,0,1,1,1,0,1,0}, {1,0,1,1,1,0,1,0}, {0,1,1,1,1,0,1,0}, {1,1,1,1,1,0,1,0}, -{0,0,0,0,0,1,1,0}, {1,0,0,0,0,1,1,0}, {0,1,0,0,0,1,1,0}, {1,1,0,0,0,1,1,0}, -{0,0,1,0,0,1,1,0}, {1,0,1,0,0,1,1,0}, {0,1,1,0,0,1,1,0}, {1,1,1,0,0,1,1,0}, -{0,0,0,1,0,1,1,0}, {1,0,0,1,0,1,1,0}, {0,1,0,1,0,1,1,0}, {1,1,0,1,0,1,1,0}, -{0,0,1,1,0,1,1,0}, {1,0,1,1,0,1,1,0}, {0,1,1,1,0,1,1,0}, {1,1,1,1,0,1,1,0}, -{0,0,0,0,1,1,1,0}, {1,0,0,0,1,1,1,0}, {0,1,0,0,1,1,1,0}, {1,1,0,0,1,1,1,0}, -{0,0,1,0,1,1,1,0}, {1,0,1,0,1,1,1,0}, {0,1,1,0,1,1,1,0}, {1,1,1,0,1,1,1,0}, -{0,0,0,1,1,1,1,0}, {1,0,0,1,1,1,1,0}, {0,1,0,1,1,1,1,0}, {1,1,0,1,1,1,1,0}, -{0,0,1,1,1,1,1,0}, {1,0,1,1,1,1,1,0}, {0,1,1,1,1,1,1,0}, {1,1,1,1,1,1,1,0}, -{0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,1}, {0,1,0,0,0,0,0,1}, {1,1,0,0,0,0,0,1}, -{0,0,1,0,0,0,0,1}, {1,0,1,0,0,0,0,1}, {0,1,1,0,0,0,0,1}, {1,1,1,0,0,0,0,1}, -{0,0,0,1,0,0,0,1}, {1,0,0,1,0,0,0,1}, {0,1,0,1,0,0,0,1}, {1,1,0,1,0,0,0,1}, -{0,0,1,1,0,0,0,1}, {1,0,1,1,0,0,0,1}, {0,1,1,1,0,0,0,1}, {1,1,1,1,0,0,0,1}, -{0,0,0,0,1,0,0,1}, {1,0,0,0,1,0,0,1}, {0,1,0,0,1,0,0,1}, {1,1,0,0,1,0,0,1}, -{0,0,1,0,1,0,0,1}, {1,0,1,0,1,0,0,1}, {0,1,1,0,1,0,0,1}, {1,1,1,0,1,0,0,1}, -{0,0,0,1,1,0,0,1}, {1,0,0,1,1,0,0,1}, {0,1,0,1,1,0,0,1}, {1,1,0,1,1,0,0,1}, -{0,0,1,1,1,0,0,1}, {1,0,1,1,1,0,0,1}, {0,1,1,1,1,0,0,1}, {1,1,1,1,1,0,0,1}, -{0,0,0,0,0,1,0,1}, {1,0,0,0,0,1,0,1}, {0,1,0,0,0,1,0,1}, {1,1,0,0,0,1,0,1}, -{0,0,1,0,0,1,0,1}, {1,0,1,0,0,1,0,1}, {0,1,1,0,0,1,0,1}, {1,1,1,0,0,1,0,1}, -{0,0,0,1,0,1,0,1}, {1,0,0,1,0,1,0,1}, {0,1,0,1,0,1,0,1}, {1,1,0,1,0,1,0,1}, -{0,0,1,1,0,1,0,1}, {1,0,1,1,0,1,0,1}, {0,1,1,1,0,1,0,1}, {1,1,1,1,0,1,0,1}, -{0,0,0,0,1,1,0,1}, {1,0,0,0,1,1,0,1}, {0,1,0,0,1,1,0,1}, {1,1,0,0,1,1,0,1}, -{0,0,1,0,1,1,0,1}, {1,0,1,0,1,1,0,1}, {0,1,1,0,1,1,0,1}, {1,1,1,0,1,1,0,1}, -{0,0,0,1,1,1,0,1}, {1,0,0,1,1,1,0,1}, {0,1,0,1,1,1,0,1}, {1,1,0,1,1,1,0,1}, -{0,0,1,1,1,1,0,1}, {1,0,1,1,1,1,0,1}, {0,1,1,1,1,1,0,1}, {1,1,1,1,1,1,0,1}, -{0,0,0,0,0,0,1,1}, {1,0,0,0,0,0,1,1}, {0,1,0,0,0,0,1,1}, {1,1,0,0,0,0,1,1}, -{0,0,1,0,0,0,1,1}, {1,0,1,0,0,0,1,1}, {0,1,1,0,0,0,1,1}, {1,1,1,0,0,0,1,1}, -{0,0,0,1,0,0,1,1}, {1,0,0,1,0,0,1,1}, {0,1,0,1,0,0,1,1}, {1,1,0,1,0,0,1,1}, -{0,0,1,1,0,0,1,1}, {1,0,1,1,0,0,1,1}, {0,1,1,1,0,0,1,1}, {1,1,1,1,0,0,1,1}, -{0,0,0,0,1,0,1,1}, {1,0,0,0,1,0,1,1}, {0,1,0,0,1,0,1,1}, {1,1,0,0,1,0,1,1}, -{0,0,1,0,1,0,1,1}, {1,0,1,0,1,0,1,1}, {0,1,1,0,1,0,1,1}, {1,1,1,0,1,0,1,1}, -{0,0,0,1,1,0,1,1}, {1,0,0,1,1,0,1,1}, {0,1,0,1,1,0,1,1}, {1,1,0,1,1,0,1,1}, -{0,0,1,1,1,0,1,1}, {1,0,1,1,1,0,1,1}, {0,1,1,1,1,0,1,1}, {1,1,1,1,1,0,1,1}, -{0,0,0,0,0,1,1,1}, {1,0,0,0,0,1,1,1}, {0,1,0,0,0,1,1,1}, {1,1,0,0,0,1,1,1}, -{0,0,1,0,0,1,1,1}, {1,0,1,0,0,1,1,1}, {0,1,1,0,0,1,1,1}, {1,1,1,0,0,1,1,1}, -{0,0,0,1,0,1,1,1}, {1,0,0,1,0,1,1,1}, {0,1,0,1,0,1,1,1}, {1,1,0,1,0,1,1,1}, -{0,0,1,1,0,1,1,1}, {1,0,1,1,0,1,1,1}, {0,1,1,1,0,1,1,1}, {1,1,1,1,0,1,1,1}, -{0,0,0,0,1,1,1,1}, {1,0,0,0,1,1,1,1}, {0,1,0,0,1,1,1,1}, {1,1,0,0,1,1,1,1}, -{0,0,1,0,1,1,1,1}, {1,0,1,0,1,1,1,1}, {0,1,1,0,1,1,1,1}, {1,1,1,0,1,1,1,1}, -{0,0,0,1,1,1,1,1}, {1,0,0,1,1,1,1,1}, {0,1,0,1,1,1,1,1}, {1,1,0,1,1,1,1,1}, -{0,0,1,1,1,1,1,1}, {1,0,1,1,1,1,1,1}, {0,1,1,1,1,1,1,1}, {1,1,1,1,1,1,1,1}} - --- Return a number that is the result of interpreting the table tbl (msb first) -local function tbl_to_number(tbl) - local n = #tbl - local rslt = 0 - local power = 1 - for i = 1, n do - rslt = rslt + tbl[i]*power - power = power*2 - end - return rslt -end - --- Calculate bitwise xor of bytes m and n. 0 <= m,n <= 256. -local function bit_xor(m, n) - local tbl_m = cclxvi[m] - local tbl_n = cclxvi[n] - local tbl = {} - for i = 1, 8 do - if(tbl_m[i] ~= tbl_n[i]) then - tbl[i] = 1 - else - tbl[i] = 0 - end - end - return tbl_to_number(tbl) -end - --- Return the binary representation of the number x with the width of `digits`. -local function binary(x,digits) - local s=string.format("%o",x) - local a={["0"]="000",["1"]="001", ["2"]="010",["3"]="011", - ["4"]="100",["5"]="101", ["6"]="110",["7"]="111"} - s=string.gsub(s,"(.)",function (d) return a[d] end) - -- remove leading 0s - s = string.gsub(s,"^0*(.*)$","%1") - local fmtstring = string.format("%%%ds",digits) - local ret = string.format(fmtstring,s) - return string.gsub(ret," ","0") -end - --- A small helper function for add_typeinfo_to_matrix() and add_version_information() --- Add a 2 (black by default) / -2 (blank by default) to the matrix at position x,y --- depending on the bitstring (size 1!) where "0"=blank and "1"=black. -local function fill_matrix_position(matrix,bitstring,x,y) - if bitstring == "1" then - matrix[x][y] = 2 - else - matrix[x][y] = -2 - end -end - - ---- Step 1: Determine version, ec level and mode for codeword ---- ======================================================== ---- ---- First we need to find out the version (= size) of the QR code. This depends on ---- the input data (the mode to be used), the requested error correction level ---- (normally we use the maximum level that fits into the minimal size). - --- Return the mode for the given string `str`. --- See table 2 of the spec. We only support mode 1, 2 and 4. --- That is: numeric, alaphnumeric and binary. -local function get_mode( str ) - if string.match(str,"^[0-9]+$") then - return 1 - elseif string.match(str,"^[0-9A-Z $%%*./:+-]+$") then - return 2 - else - return 4 - end - assert(false,"never reached") -- luacheck: ignore - return nil -end - - - ---- Capacity of QR codes ---- -------------------- ---- The capacity is calculated as follow: \\(\text{Number of data bits} = \text{number of codewords} * 8\\). ---- The number of data bits is now reduced by 4 (the mode indicator) and the length string, ---- that varies between 8 and 16, depending on the version and the mode (see method `get_length()`). The ---- remaining capacity is multiplied by the amount of data per bit string (numeric: 3, alphanumeric: 2, other: 1) ---- and divided by the length of the bit string (numeric: 10, alphanumeric: 11, binary: 8, kanji: 13). ---- Then the floor function is applied to the result: ---- $$\Big\lfloor \frac{( \text{#data bits} - 4 - \text{length string}) * \text{data per bit string}}{\text{length of the bit string}} \Big\rfloor$$ ---- ---- There is one problem remaining. The length string depends on the version, ---- and the version depends on the length string. But we take this into account when calculating the ---- the capacity, so this is not really a problem here. - --- The capacity (number of codewords) of each version (1-40) for error correction levels 1-4 (LMQH). --- The higher the ec level, the lower the capacity of the version. Taken from spec, tables 7-11. -local capacity = { - { 19, 16, 13, 9},{ 34, 28, 22, 16},{ 55, 44, 34, 26},{ 80, 64, 48, 36}, - { 108, 86, 62, 46},{ 136, 108, 76, 60},{ 156, 124, 88, 66},{ 194, 154, 110, 86}, - { 232, 182, 132, 100},{ 274, 216, 154, 122},{ 324, 254, 180, 140},{ 370, 290, 206, 158}, - { 428, 334, 244, 180},{ 461, 365, 261, 197},{ 523, 415, 295, 223},{ 589, 453, 325, 253}, - { 647, 507, 367, 283},{ 721, 563, 397, 313},{ 795, 627, 445, 341},{ 861, 669, 485, 385}, - { 932, 714, 512, 406},{1006, 782, 568, 442},{1094, 860, 614, 464},{1174, 914, 664, 514}, - {1276, 1000, 718, 538},{1370, 1062, 754, 596},{1468, 1128, 808, 628},{1531, 1193, 871, 661}, - {1631, 1267, 911, 701},{1735, 1373, 985, 745},{1843, 1455, 1033, 793},{1955, 1541, 1115, 845}, - {2071, 1631, 1171, 901},{2191, 1725, 1231, 961},{2306, 1812, 1286, 986},{2434, 1914, 1354, 1054}, - {2566, 1992, 1426, 1096},{2702, 2102, 1502, 1142},{2812, 2216, 1582, 1222},{2956, 2334, 1666, 1276}} - - ---- Return the smallest version for this codeword. If `requested_ec_level` is supplied, ---- then the ec level (LMQH - 1,2,3,4) must be at least the requested level. --- mode = 1,2,4,8 -local function get_version_eclevel(len,mode,requested_ec_level) - local local_mode = mode - if mode == 4 then - local_mode = 3 - elseif mode == 8 then - local_mode = 4 - end - assert( local_mode <= 4 ) - - local bits, digits, modebits, c - local tab = { {10,9,8,8},{12,11,16,10},{14,13,16,12} } - local minversion = 40 - local maxec_level = requested_ec_level or 1 - local min,max = 1, 4 - if requested_ec_level and requested_ec_level >= 1 and requested_ec_level <= 4 then - min = requested_ec_level - max = requested_ec_level - end - for ec_level=min,max do - for version=1,#capacity do - bits = capacity[version][ec_level] * 8 - bits = bits - 4 -- the mode indicator - if version < 10 then - digits = tab[1][local_mode] - elseif version < 27 then - digits = tab[2][local_mode] - elseif version <= 40 then - digits = tab[3][local_mode] - end - modebits = bits - digits - if local_mode == 1 then -- numeric - c = math.floor(modebits * 3 / 10) - elseif local_mode == 2 then -- alphanumeric - c = math.floor(modebits * 2 / 11) - elseif local_mode == 3 then -- binary - c = math.floor(modebits * 1 / 8) - else - c = math.floor(modebits * 1 / 13) - end - if c >= len then - if version <= minversion then - minversion = version - maxec_level = ec_level - end - break - end - end - end - return minversion, maxec_level -end - --- Return a bit string of 0s and 1s that includes the length of the code string. --- The modes are numeric = 1, alphanumeric = 2, binary = 4, and japanese = 8 -local function get_length(str,version,mode) - local i = mode - if mode == 4 then - i = 3 - elseif mode == 8 then - i = 4 - end - assert( i <= 4 ) - local tab = { {10,9,8,8},{12,11,16,10},{14,13,16,12} } - local digits - if version < 10 then - digits = tab[1][i] - elseif version < 27 then - digits = tab[2][i] - elseif version <= 40 then - digits = tab[3][i] - else - assert(false, "get_length, version > 40 not supported") - end - local len = binary(#str,digits) - return len -end - ---- If the `requested_ec_level` or the `mode` are provided, this will be used if possible. ---- The mode depends on the characters used in the string `str`. It seems to be ---- possible to split the QR code to handle multiple modes, but we don't do that. -local function get_version_eclevel_mode_bistringlength(str,requested_ec_level,mode) - local local_mode - if mode then - assert(false,"not implemented") - -- check if the mode is OK for the string - local_mode = mode - else - local_mode = get_mode(str) - end - local version, ec_level - version, ec_level = get_version_eclevel(#str,local_mode,requested_ec_level) - local length_string = get_length(str,version,local_mode) - return version,ec_level,binary(local_mode,4),local_mode,length_string -end - ---- Step 2: Encode data ---- =================== - ---- There are several ways to encode the data. We currently support only numeric, alphanumeric and binary. ---- We already chose the encoding (a.k.a. mode) in the first step, so we need to apply the mode to the ---- codeword. ---- ---- **Numeric**: take three digits and encode them in 10 bits ---- **Alphanumeric**: take two characters and encode them in 11 bits ---- **Binary**: take one octet and encode it in 8 bits - -local asciitbl = { - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -- 0x01-0x0f - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -- 0x10-0x1f - 36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, -- 0x20-0x2f - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1, -- 0x30-0x3f - -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, -- 0x40-0x4f - 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, -- 0x50-0x5f - } - --- Return a binary representation of the numeric string `str`. This must contain only digits 0-9. -local function encode_string_numeric(str) - local bitstring = "" - local int - string.gsub(str,"..?.?",function(a) - int = tonumber(a) - if #a == 3 then - bitstring = bitstring .. binary(int,10) - elseif #a == 2 then - bitstring = bitstring .. binary(int,7) - else - bitstring = bitstring .. binary(int,4) - end - end) - return bitstring -end - --- Return a binary representation of the alphanumeric string `str`. This must contain only --- digits 0-9, uppercase letters A-Z, space and the following chars: $%*./:+-. -local function encode_string_ascii(str) - local bitstring = "" - local int - local b1, b2 - string.gsub(str,"..?",function(a) - if #a == 2 then - b1 = asciitbl[string.byte(string.sub(a,1,1))] - b2 = asciitbl[string.byte(string.sub(a,2,2))] - int = b1 * 45 + b2 - bitstring = bitstring .. binary(int,11) - else - int = asciitbl[string.byte(a)] - bitstring = bitstring .. binary(int,6) - end - end) - return bitstring -end - --- Return a bitstring representing string str in binary mode. --- We don't handle UTF-8 in any special way because we assume the --- scanner recognizes UTF-8 and displays it correctly. -local function encode_string_binary(str) - local ret = {} - string.gsub(str,".",function(x) - ret[#ret + 1] = binary(string.byte(x),8) - end) - return table.concat(ret) -end - --- Return a bitstring representing string str in the given mode. -local function encode_data(str,mode) - if mode == 1 then - return encode_string_numeric(str) - elseif mode == 2 then - return encode_string_ascii(str) - elseif mode == 4 then - return encode_string_binary(str) - else - assert(false,"not implemented yet") - end -end - --- Encoding the codeword is not enough. We need to make sure that --- the length of the binary string is equal to the number of codewords of the version. -local function add_pad_data(version,ec_level,data) - local count_to_pad, missing_digits - local cpty = capacity[version][ec_level] * 8 - count_to_pad = math.min(4,cpty - #data) - if count_to_pad > 0 then - data = data .. string.rep("0",count_to_pad) - end - if math.fmod(#data,8) ~= 0 then - missing_digits = 8 - math.fmod(#data,8) - data = data .. string.rep("0",missing_digits) - end - assert(math.fmod(#data,8) == 0) - -- add "11101100" and "00010001" until enough data - while #data < cpty do - data = data .. "11101100" - if #data < cpty then - data = data .. "00010001" - end - end - return data -end - - - ---- Step 3: Organize data and calculate error correction code ---- ======================================================= ---- The data in the qrcode is not encoded linearly. For example code 5-H has four blocks, the first two blocks ---- contain 11 codewords and 22 error correction codes each, the second block contain 12 codewords and 22 ec codes each. ---- We just take the table from the spec and don't calculate the blocks ourself. The table `ecblocks` contains this info. ---- ---- During the phase of splitting the data into codewords, we do the calculation for error correction codes. This step involves ---- polynomial division. Find a math book from school and follow the code here :) - ---- ### Reed Solomon error correction ---- Now this is the slightly ugly part of the error correction. We start with log/antilog tables --- https://codyplanteen.com/assets/rs/gf256_log_antilog.pdf -local alpha_int = { - [0] = 1, - 2, 4, 8, 16, 32, 64, 128, 29, 58, 116, 232, 205, 135, 19, 38, 76, - 152, 45, 90, 180, 117, 234, 201, 143, 3, 6, 12, 24, 48, 96, 192, 157, - 39, 78, 156, 37, 74, 148, 53, 106, 212, 181, 119, 238, 193, 159, 35, 70, - 140, 5, 10, 20, 40, 80, 160, 93, 186, 105, 210, 185, 111, 222, 161, 95, - 190, 97, 194, 153, 47, 94, 188, 101, 202, 137, 15, 30, 60, 120, 240, 253, - 231, 211, 187, 107, 214, 177, 127, 254, 225, 223, 163, 91, 182, 113, 226, 217, - 175, 67, 134, 17, 34, 68, 136, 13, 26, 52, 104, 208, 189, 103, 206, 129, - 31, 62, 124, 248, 237, 199, 147, 59, 118, 236, 197, 151, 51, 102, 204, 133, - 23, 46, 92, 184, 109, 218, 169, 79, 158, 33, 66, 132, 21, 42, 84, 168, - 77, 154, 41, 82, 164, 85, 170, 73, 146, 57, 114, 228, 213, 183, 115, 230, - 209, 191, 99, 198, 145, 63, 126, 252, 229, 215, 179, 123, 246, 241, 255, 227, - 219, 171, 75, 150, 49, 98, 196, 149, 55, 110, 220, 165, 87, 174, 65, 130, - 25, 50, 100, 200, 141, 7, 14, 28, 56, 112, 224, 221, 167, 83, 166, 81, - 162, 89, 178, 121, 242, 249, 239, 195, 155, 43, 86, 172, 69, 138, 9, 18, - 36, 72, 144, 61, 122, 244, 245, 247, 243, 251, 235, 203, 139, 11, 22, 44, - 88, 176, 125, 250, 233, 207, 131, 27, 54, 108, 216, 173, 71, 142, 0, 0 -} - -local int_alpha = { - [0] = 256, -- special value - 0, 1, 25, 2, 50, 26, 198, 3, 223, 51, 238, 27, 104, 199, 75, 4, - 100, 224, 14, 52, 141, 239, 129, 28, 193, 105, 248, 200, 8, 76, 113, 5, - 138, 101, 47, 225, 36, 15, 33, 53, 147, 142, 218, 240, 18, 130, 69, 29, - 181, 194, 125, 106, 39, 249, 185, 201, 154, 9, 120, 77, 228, 114, 166, 6, - 191, 139, 98, 102, 221, 48, 253, 226, 152, 37, 179, 16, 145, 34, 136, 54, - 208, 148, 206, 143, 150, 219, 189, 241, 210, 19, 92, 131, 56, 70, 64, 30, - 66, 182, 163, 195, 72, 126, 110, 107, 58, 40, 84, 250, 133, 186, 61, 202, - 94, 155, 159, 10, 21, 121, 43, 78, 212, 229, 172, 115, 243, 167, 87, 7, - 112, 192, 247, 140, 128, 99, 13, 103, 74, 222, 237, 49, 197, 254, 24, 227, - 165, 153, 119, 38, 184, 180, 124, 17, 68, 146, 217, 35, 32, 137, 46, 55, - 63, 209, 91, 149, 188, 207, 205, 144, 135, 151, 178, 220, 252, 190, 97, 242, - 86, 211, 171, 20, 42, 93, 158, 132, 60, 57, 83, 71, 109, 65, 162, 31, - 45, 67, 216, 183, 123, 164, 118, 196, 23, 73, 236, 127, 12, 111, 246, 108, - 161, 59, 82, 41, 157, 85, 170, 251, 96, 134, 177, 187, 204, 62, 90, 203, - 89, 95, 176, 156, 169, 160, 81, 11, 245, 22, 235, 122, 117, 44, 215, 79, - 174, 213, 233, 230, 231, 173, 232, 116, 214, 244, 234, 168, 80, 88, 175 -} - --- We only need the polynomial generators for block sizes 7, 10, 13, 15, 16, 17, 18, 20, 22, 24, 26, 28, and 30. Version --- 2 of the qr codes don't need larger ones (as opposed to version 1). The table has the format x^1*ɑ^21 + x^2*a^102 ... -local generator_polynomial = { - [7] = { 21, 102, 238, 149, 146, 229, 87, 0}, - [10] = { 45, 32, 94, 64, 70, 118, 61, 46, 67, 251, 0 }, - [13] = { 78, 140, 206, 218, 130, 104, 106, 100, 86, 100, 176, 152, 74, 0 }, - [15] = {105, 99, 5, 124, 140, 237, 58, 58, 51, 37, 202, 91, 61, 183, 8, 0}, - [16] = {120, 225, 194, 182, 169, 147, 191, 91, 3, 76, 161, 102, 109, 107, 104, 120, 0}, - [17] = {136, 163, 243, 39, 150, 99, 24, 147, 214, 206, 123, 239, 43, 78, 206, 139, 43, 0}, - [18] = {153, 96, 98, 5, 179, 252, 148, 152, 187, 79, 170, 118, 97, 184, 94, 158, 234, 215, 0}, - [20] = {190, 188, 212, 212, 164, 156, 239, 83, 225, 221, 180, 202, 187, 26, 163, 61, 50, 79, 60, 17, 0}, - [22] = {231, 165, 105, 160, 134, 219, 80, 98, 172, 8, 74, 200, 53, 221, 109, 14, 230, 93, 242, 247, 171, 210, 0}, - [24] = { 21, 227, 96, 87, 232, 117, 0, 111, 218, 228, 226, 192, 152, 169, 180, 159, 126, 251, 117, 211, 48, 135, 121, 229, 0}, - [26] = { 70, 218, 145, 153, 227, 48, 102, 13, 142, 245, 21, 161, 53, 165, 28, 111, 201, 145, 17, 118, 182, 103, 2, 158, 125, 173, 0}, - [28] = {123, 9, 37, 242, 119, 212, 195, 42, 87, 245, 43, 21, 201, 232, 27, 205, 147, 195, 190, 110, 180, 108, 234, 224, 104, 200, 223, 168, 0}, - [30] = {180, 192, 40, 238, 216, 251, 37, 156, 130, 224, 193, 226, 173, 42, 125, 222, 96, 239, 86, 110, 48, 50, 182, 179, 31, 216, 152, 145, 173, 41, 0}} - - --- Turn a binary string of length 8*x into a table size x of numbers. -local function convert_bitstring_to_bytes(data) - local msg = {} - string.gsub(data,"(........)",function(x) - msg[#msg+1] = tonumber(x,2) - end) - return msg -end - --- Return a table that has 0's in the first entries and then the alpha --- representation of the generator polynominal -local function get_generator_polynominal_adjusted(num_ec_codewords,highest_exponent) - local gp_alpha = {[0]=0} - for i=0,highest_exponent - num_ec_codewords - 1 do - gp_alpha[i] = 0 - end - local gp = generator_polynomial[num_ec_codewords] - for i=1,num_ec_codewords + 1 do - gp_alpha[highest_exponent - num_ec_codewords + i - 1] = gp[i] - end - return gp_alpha -end - ---- These converter functions use the log/antilog table above. ---- We could have created the table programatically, but I like fixed tables. --- Convert polynominal in int notation to alpha notation. -local function convert_to_alpha( tab ) - local new_tab = {} - for i=0,#tab do - new_tab[i] = int_alpha[tab[i]] - end - return new_tab -end - --- Convert polynominal in alpha notation to int notation. -local function convert_to_int(tab) - local new_tab = {} - for i=0,#tab do - new_tab[i] = alpha_int[tab[i]] - end - return new_tab -end - --- That's the heart of the error correction calculation. -local function calculate_error_correction(data,num_ec_codewords) - local mp - if type(data)=="string" then - mp = convert_bitstring_to_bytes(data) - elseif type(data)=="table" then - mp = data - else - assert(false,string.format("Unknown type for data: %s",type(data))) - end - local len_message = #mp - - local highest_exponent = len_message + num_ec_codewords - 1 - local gp_alpha,tmp - local he - local gp_int, mp_alpha - local mp_int = {} - -- create message shifted to left (highest exponent) - for i=1,len_message do - mp_int[highest_exponent - i + 1] = mp[i] - end - for i=1,highest_exponent - len_message do - mp_int[i] = 0 - end - mp_int[0] = 0 - - mp_alpha = convert_to_alpha(mp_int) - - while highest_exponent >= num_ec_codewords do - gp_alpha = get_generator_polynominal_adjusted(num_ec_codewords,highest_exponent) - - -- Multiply generator polynomial by first coefficient of the above polynomial - - -- take the highest exponent from the message polynom (alpha) and add - -- it to the generator polynom - local exp = mp_alpha[highest_exponent] - for i=highest_exponent,highest_exponent - num_ec_codewords,-1 do - if exp ~= 256 then - if gp_alpha[i] + exp >= 255 then - gp_alpha[i] = math.fmod(gp_alpha[i] + exp,255) - else - gp_alpha[i] = gp_alpha[i] + exp - end - else - gp_alpha[i] = 256 - end - end - for i=highest_exponent - num_ec_codewords - 1,0,-1 do - gp_alpha[i] = 256 - end - - gp_int = convert_to_int(gp_alpha) - mp_int = convert_to_int(mp_alpha) - - - tmp = {} - for i=highest_exponent,0,-1 do - tmp[i] = bit_xor(gp_int[i],mp_int[i]) - end - -- remove leading 0's - he = highest_exponent - for i=he,0,-1 do - -- We need to stop if the length of the codeword is matched - if i < num_ec_codewords then break end - if tmp[i] == 0 then - tmp[i] = nil - highest_exponent = highest_exponent - 1 - else - break - end - end - mp_int = tmp - mp_alpha = convert_to_alpha(mp_int) - end - local ret = {} - - -- reverse data - for i=#mp_int,0,-1 do - ret[#ret + 1] = mp_int[i] - end - return ret -end - ---- #### Arranging the data ---- Now we arrange the data into smaller chunks. This table is taken from the spec. --- ecblocks has 40 entries, one for each version. Each version entry has 4 entries, for each LMQH --- ec level. Each entry has two or four fields, the odd files are the number of repetitions for the --- folowing block info. The first entry of the block is the total number of codewords in the block, --- the second entry is the number of data codewords. The third is not important. -local ecblocks = { - {{ 1,{ 26, 19, 2} }, { 1,{26,16, 4}}, { 1,{26,13, 6}}, { 1, {26, 9, 8} }}, - {{ 1,{ 44, 34, 4} }, { 1,{44,28, 8}}, { 1,{44,22,11}}, { 1, {44,16,14} }}, - {{ 1,{ 70, 55, 7} }, { 1,{70,44,13}}, { 2,{35,17, 9}}, { 2, {35,13,11} }}, - {{ 1,{100, 80,10} }, { 2,{50,32, 9}}, { 2,{50,24,13}}, { 4, {25, 9, 8} }}, - {{ 1,{134,108,13} }, { 2,{67,43,12}}, { 2,{33,15, 9}, 2,{34,16, 9}}, { 2, {33,11,11}, 2,{34,12,11}}}, - {{ 2,{ 86, 68, 9} }, { 4,{43,27, 8}}, { 4,{43,19,12}}, { 4, {43,15,14} }}, - {{ 2,{ 98, 78,10} }, { 4,{49,31, 9}}, { 2,{32,14, 9}, 4,{33,15, 9}}, { 4, {39,13,13}, 1,{40,14,13}}}, - {{ 2,{121, 97,12} }, { 2,{60,38,11}, 2,{61,39,11}}, { 4,{40,18,11}, 2,{41,19,11}}, { 4, {40,14,13}, 2,{41,15,13}}}, - {{ 2,{146,116,15} }, { 3,{58,36,11}, 2,{59,37,11}}, { 4,{36,16,10}, 4,{37,17,10}}, { 4, {36,12,12}, 4,{37,13,12}}}, - {{ 2,{ 86, 68, 9}, 2,{ 87, 69, 9}}, { 4,{69,43,13}, 1,{70,44,13}}, { 6,{43,19,12}, 2,{44,20,12}}, { 6, {43,15,14}, 2,{44,16,14}}}, - {{ 4,{101, 81,10} }, { 1,{80,50,15}, 4,{81,51,15}}, { 4,{50,22,14}, 4,{51,23,14}}, { 3, {36,12,12}, 8,{37,13,12}}}, - {{ 2,{116, 92,12}, 2,{117, 93,12}}, { 6,{58,36,11}, 2,{59,37,11}}, { 4,{46,20,13}, 6,{47,21,13}}, { 7, {42,14,14}, 4,{43,15,14}}}, - {{ 4,{133,107,13} }, { 8,{59,37,11}, 1,{60,38,11}}, { 8,{44,20,12}, 4,{45,21,12}}, { 12, {33,11,11}, 4,{34,12,11}}}, - {{ 3,{145,115,15}, 1,{146,116,15}}, { 4,{64,40,12}, 5,{65,41,12}}, { 11,{36,16,10}, 5,{37,17,10}}, { 11, {36,12,12}, 5,{37,13,12}}}, - {{ 5,{109, 87,11}, 1,{110, 88,11}}, { 5,{65,41,12}, 5,{66,42,12}}, { 5,{54,24,15}, 7,{55,25,15}}, { 11, {36,12,12}, 7,{37,13,12}}}, - {{ 5,{122, 98,12}, 1,{123, 99,12}}, { 7,{73,45,14}, 3,{74,46,14}}, { 15,{43,19,12}, 2,{44,20,12}}, { 3, {45,15,15}, 13,{46,16,15}}}, - {{ 1,{135,107,14}, 5,{136,108,14}}, { 10,{74,46,14}, 1,{75,47,14}}, { 1,{50,22,14}, 15,{51,23,14}}, { 2, {42,14,14}, 17,{43,15,14}}}, - {{ 5,{150,120,15}, 1,{151,121,15}}, { 9,{69,43,13}, 4,{70,44,13}}, { 17,{50,22,14}, 1,{51,23,14}}, { 2, {42,14,14}, 19,{43,15,14}}}, - {{ 3,{141,113,14}, 4,{142,114,14}}, { 3,{70,44,13}, 11,{71,45,13}}, { 17,{47,21,13}, 4,{48,22,13}}, { 9, {39,13,13}, 16,{40,14,13}}}, - {{ 3,{135,107,14}, 5,{136,108,14}}, { 3,{67,41,13}, 13,{68,42,13}}, { 15,{54,24,15}, 5,{55,25,15}}, { 15, {43,15,14}, 10,{44,16,14}}}, - {{ 4,{144,116,14}, 4,{145,117,14}}, { 17,{68,42,13}}, { 17,{50,22,14}, 6,{51,23,14}}, { 19, {46,16,15}, 6,{47,17,15}}}, - {{ 2,{139,111,14}, 7,{140,112,14}}, { 17,{74,46,14}}, { 7,{54,24,15}, 16,{55,25,15}}, { 34, {37,13,12} }}, - {{ 4,{151,121,15}, 5,{152,122,15}}, { 4,{75,47,14}, 14,{76,48,14}}, { 11,{54,24,15}, 14,{55,25,15}}, { 16, {45,15,15}, 14,{46,16,15}}}, - {{ 6,{147,117,15}, 4,{148,118,15}}, { 6,{73,45,14}, 14,{74,46,14}}, { 11,{54,24,15}, 16,{55,25,15}}, { 30, {46,16,15}, 2,{47,17,15}}}, - {{ 8,{132,106,13}, 4,{133,107,13}}, { 8,{75,47,14}, 13,{76,48,14}}, { 7,{54,24,15}, 22,{55,25,15}}, { 22, {45,15,15}, 13,{46,16,15}}}, - {{ 10,{142,114,14}, 2,{143,115,14}}, { 19,{74,46,14}, 4,{75,47,14}}, { 28,{50,22,14}, 6,{51,23,14}}, { 33, {46,16,15}, 4,{47,17,15}}}, - {{ 8,{152,122,15}, 4,{153,123,15}}, { 22,{73,45,14}, 3,{74,46,14}}, { 8,{53,23,15}, 26,{54,24,15}}, { 12, {45,15,15}, 28,{46,16,15}}}, - {{ 3,{147,117,15}, 10,{148,118,15}}, { 3,{73,45,14}, 23,{74,46,14}}, { 4,{54,24,15}, 31,{55,25,15}}, { 11, {45,15,15}, 31,{46,16,15}}}, - {{ 7,{146,116,15}, 7,{147,117,15}}, { 21,{73,45,14}, 7,{74,46,14}}, { 1,{53,23,15}, 37,{54,24,15}}, { 19, {45,15,15}, 26,{46,16,15}}}, - {{ 5,{145,115,15}, 10,{146,116,15}}, { 19,{75,47,14}, 10,{76,48,14}}, { 15,{54,24,15}, 25,{55,25,15}}, { 23, {45,15,15}, 25,{46,16,15}}}, - {{ 13,{145,115,15}, 3,{146,116,15}}, { 2,{74,46,14}, 29,{75,47,14}}, { 42,{54,24,15}, 1,{55,25,15}}, { 23, {45,15,15}, 28,{46,16,15}}}, - {{ 17,{145,115,15} }, { 10,{74,46,14}, 23,{75,47,14}}, { 10,{54,24,15}, 35,{55,25,15}}, { 19, {45,15,15}, 35,{46,16,15}}}, - {{ 17,{145,115,15}, 1,{146,116,15}}, { 14,{74,46,14}, 21,{75,47,14}}, { 29,{54,24,15}, 19,{55,25,15}}, { 11, {45,15,15}, 46,{46,16,15}}}, - {{ 13,{145,115,15}, 6,{146,116,15}}, { 14,{74,46,14}, 23,{75,47,14}}, { 44,{54,24,15}, 7,{55,25,15}}, { 59, {46,16,15}, 1,{47,17,15}}}, - {{ 12,{151,121,15}, 7,{152,122,15}}, { 12,{75,47,14}, 26,{76,48,14}}, { 39,{54,24,15}, 14,{55,25,15}}, { 22, {45,15,15}, 41,{46,16,15}}}, - {{ 6,{151,121,15}, 14,{152,122,15}}, { 6,{75,47,14}, 34,{76,48,14}}, { 46,{54,24,15}, 10,{55,25,15}}, { 2, {45,15,15}, 64,{46,16,15}}}, - {{ 17,{152,122,15}, 4,{153,123,15}}, { 29,{74,46,14}, 14,{75,47,14}}, { 49,{54,24,15}, 10,{55,25,15}}, { 24, {45,15,15}, 46,{46,16,15}}}, - {{ 4,{152,122,15}, 18,{153,123,15}}, { 13,{74,46,14}, 32,{75,47,14}}, { 48,{54,24,15}, 14,{55,25,15}}, { 42, {45,15,15}, 32,{46,16,15}}}, - {{ 20,{147,117,15}, 4,{148,118,15}}, { 40,{75,47,14}, 7,{76,48,14}}, { 43,{54,24,15}, 22,{55,25,15}}, { 10, {45,15,15}, 67,{46,16,15}}}, - {{ 19,{148,118,15}, 6,{149,119,15}}, { 18,{75,47,14}, 31,{76,48,14}}, { 34,{54,24,15}, 34,{55,25,15}}, { 20, {45,15,15}, 61,{46,16,15}}} -} - --- The bits that must be 0 if the version does fill the complete matrix. --- Example: for version 1, no bits need to be added after arranging the data, for version 2 we need to add 7 bits at the end. -local remainder = {0, 7, 7, 7, 7, 7, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0} - --- This is the formula for table 1 in the spec: --- function get_capacity_remainder( version ) --- local len = version * 4 + 17 --- local size = len^2 --- local function_pattern_modules = 192 + 2 * len - 32 -- Position Adjustment pattern + timing pattern --- local count_alignemnt_pattern = #alignment_pattern[version] --- if count_alignemnt_pattern > 0 then --- -- add 25 for each aligment pattern --- function_pattern_modules = function_pattern_modules + 25 * ( count_alignemnt_pattern^2 - 3 ) --- -- but substract the timing pattern occupied by the aligment pattern on the top and left --- function_pattern_modules = function_pattern_modules - ( count_alignemnt_pattern - 2) * 10 --- end --- size = size - function_pattern_modules --- if version > 6 then --- size = size - 67 --- else --- size = size - 31 --- end --- return math.floor(size/8),math.fmod(size,8) --- end - - ---- Example: Version 5-H has four data and four error correction blocks. The table above lists ---- `2, {33,11,11}, 2,{34,12,11}` for entry [5][4]. This means we take two blocks with 11 codewords ---- and two blocks with 12 codewords, and two blocks with 33 - 11 = 22 ec codes and another ---- two blocks with 34 - 12 = 22 ec codes. ---- Block 1: D1 D2 D3 ... D11 ---- Block 2: D12 D13 D14 ... D22 ---- Block 3: D23 D24 D25 ... D33 D34 ---- Block 4: D35 D36 D37 ... D45 D46 ---- Then we place the data like this in the matrix: D1, D12, D23, D35, D2, D13, D24, D36 ... D45, D34, D46. The same goes ---- with error correction codes. - --- The given data can be a string of 0's and 1' (with #string mod 8 == 0). --- Alternatively the data can be a table of codewords. The number of codewords --- must match the capacity of the qr code. -local function arrange_codewords_and_calculate_ec( version,ec_level,data ) - if type(data)=="table" then - local tmp = "" - for i=1,#data do - tmp = tmp .. binary(data[i],8) - end - data = tmp - end - -- If the size of the data is not enough for the codeword, we add 0's and two special bytes until finished. - local blocks = ecblocks[version][ec_level] - local size_datablock_bytes, size_ecblock_bytes - local datablocks = {} - local final_ecblocks = {} - local count = 1 - local pos = 0 - local cpty_ec_bits = 0 - for i=1,#blocks/2 do - for _=1,blocks[2*i - 1] do - size_datablock_bytes = blocks[2*i][2] - size_ecblock_bytes = blocks[2*i][1] - blocks[2*i][2] - cpty_ec_bits = cpty_ec_bits + size_ecblock_bytes * 8 - datablocks[#datablocks + 1] = string.sub(data, pos * 8 + 1,( pos + size_datablock_bytes)*8) - local tmp_tab = calculate_error_correction(datablocks[#datablocks],size_ecblock_bytes) - local tmp_str = "" - for x=1,#tmp_tab do - tmp_str = tmp_str .. binary(tmp_tab[x],8) - end - final_ecblocks[#final_ecblocks + 1] = tmp_str - pos = pos + size_datablock_bytes - count = count + 1 - end - end - local arranged_data = "" - pos = 1 - repeat - for i=1,#datablocks do - if pos < #datablocks[i] then - arranged_data = arranged_data .. string.sub(datablocks[i],pos, pos + 7) - end - end - pos = pos + 8 - until #arranged_data == #data - -- ec - local arranged_ec = "" - pos = 1 - repeat - for i=1,#final_ecblocks do - if pos < #final_ecblocks[i] then - arranged_ec = arranged_ec .. string.sub(final_ecblocks[i],pos, pos + 7) - end - end - pos = pos + 8 - until #arranged_ec == cpty_ec_bits - return arranged_data .. arranged_ec -end - ---- Step 4: Generate 8 matrices with different masks and calculate the penalty ---- ========================================================================== ---- ---- Prepare matrix ---- -------------- ---- The first step is to prepare an _empty_ matrix for a given size/mask. The matrix has a ---- few predefined areas that must be black or blank. We encode the matrix with a two ---- dimensional field where the numbers determine which pixel is blank or not. ---- ---- The following code is used for our matrix: ---- 0 = not in use yet, ---- -2 = blank by mandatory pattern, ---- 2 = black by mandatory pattern, ---- -1 = blank by data, ---- 1 = black by data ---- ---- ---- To prepare the _empty_, we add positioning, alingment and timing patters. - ---- ### Positioning patterns ### -local function add_position_detection_patterns(tab_x) - local size = #tab_x - -- allocate quite zone in the matrix area - for i=1,8 do - for j=1,8 do - tab_x[i][j] = -2 - tab_x[size - 8 + i][j] = -2 - tab_x[i][size - 8 + j] = -2 - end - end - -- draw the detection pattern (outer) - for i=1,7 do - -- top left - tab_x[1][i]=2 - tab_x[7][i]=2 - tab_x[i][1]=2 - tab_x[i][7]=2 - - -- top right - tab_x[size][i]=2 - tab_x[size - 6][i]=2 - tab_x[size - i + 1][1]=2 - tab_x[size - i + 1][7]=2 - - -- bottom left - tab_x[1][size - i + 1]=2 - tab_x[7][size - i + 1]=2 - tab_x[i][size - 6]=2 - tab_x[i][size]=2 - end - -- draw the detection pattern (inner) - for i=1,3 do - for j=1,3 do - -- top left - tab_x[2+j][i+2]=2 - -- top right - tab_x[size - j - 1][i+2]=2 - -- bottom left - tab_x[2 + j][size - i - 1]=2 - end - end -end - ---- ### Timing patterns ### --- The timing patterns (two) are the dashed lines between two adjacent positioning patterns on row/column 7. -local function add_timing_pattern(tab_x) - local line,col - line = 7 - col = 9 - for i=col,#tab_x - 8 do - if math.fmod(i,2) == 1 then - tab_x[i][line] = 2 - else - tab_x[i][line] = -2 - end - end - for i=col,#tab_x - 8 do - if math.fmod(i,2) == 1 then - tab_x[line][i] = 2 - else - tab_x[line][i] = -2 - end - end -end - - ---- ### Alignment patterns ### ---- The alignment patterns must be added to the matrix for versions > 1. The amount and positions depend on the versions and are ---- given by the spec. Beware: the patterns must not be placed where we have the positioning patterns ---- (that is: top left, top right and bottom left.) - --- For each version, where should we place the alignment patterns? See table E.1 of the spec -local alignment_pattern = { - {},{6,18},{6,22},{6,26},{6,30},{6,34}, -- 1-6 - {6,22,38},{6,24,42},{6,26,46},{6,28,50},{6,30,54},{6,32,58},{6,34,62}, -- 7-13 - {6,26,46,66},{6,26,48,70},{6,26,50,74},{6,30,54,78},{6,30,56,82},{6,30,58,86},{6,34,62,90}, -- 14-20 - {6,28,50,72,94},{6,26,50,74,98},{6,30,54,78,102},{6,28,54,80,106},{6,32,58,84,110},{6,30,58,86,114},{6,34,62,90,118}, -- 21-27 - {6,26,50,74,98 ,122},{6,30,54,78,102,126},{6,26,52,78,104,130},{6,30,56,82,108,134},{6,34,60,86,112,138},{6,30,58,86,114,142},{6,34,62,90,118,146}, -- 28-34 - {6,30,54,78,102,126,150}, {6,24,50,76,102,128,154},{6,28,54,80,106,132,158},{6,32,58,84,110,136,162},{6,26,54,82,110,138,166},{6,30,58,86,114,142,170} -- 35 - 40 -} - ---- The alignment pattern has size 5x5 and looks like this: ---- XXXXX ---- X X ---- X X X ---- X X ---- XXXXX -local function add_alignment_pattern( tab_x ) - local version = (#tab_x - 17) / 4 - local ap = alignment_pattern[version] - local pos_x, pos_y - for x=1,#ap do - for y=1,#ap do - -- we must not put an alignment pattern on top of the positioning pattern - if not (x == 1 and y == 1 or x == #ap and y == 1 or x == 1 and y == #ap ) then - pos_x = ap[x] + 1 - pos_y = ap[y] + 1 - tab_x[pos_x][pos_y] = 2 - tab_x[pos_x+1][pos_y] = -2 - tab_x[pos_x-1][pos_y] = -2 - tab_x[pos_x+2][pos_y] = 2 - tab_x[pos_x-2][pos_y] = 2 - tab_x[pos_x ][pos_y - 2] = 2 - tab_x[pos_x+1][pos_y - 2] = 2 - tab_x[pos_x-1][pos_y - 2] = 2 - tab_x[pos_x+2][pos_y - 2] = 2 - tab_x[pos_x-2][pos_y - 2] = 2 - tab_x[pos_x ][pos_y + 2] = 2 - tab_x[pos_x+1][pos_y + 2] = 2 - tab_x[pos_x-1][pos_y + 2] = 2 - tab_x[pos_x+2][pos_y + 2] = 2 - tab_x[pos_x-2][pos_y + 2] = 2 - - tab_x[pos_x ][pos_y - 1] = -2 - tab_x[pos_x+1][pos_y - 1] = -2 - tab_x[pos_x-1][pos_y - 1] = -2 - tab_x[pos_x+2][pos_y - 1] = 2 - tab_x[pos_x-2][pos_y - 1] = 2 - tab_x[pos_x ][pos_y + 1] = -2 - tab_x[pos_x+1][pos_y + 1] = -2 - tab_x[pos_x-1][pos_y + 1] = -2 - tab_x[pos_x+2][pos_y + 1] = 2 - tab_x[pos_x-2][pos_y + 1] = 2 - end - end - end -end - ---- ### Type information ### ---- Let's not forget the type information that is in column 9 next to the left positioning patterns and on row 9 below ---- the top positioning patterns. This type information is not fixed, it depends on the mask and the error correction. - --- The first index is ec level (LMQH,1-4), the second is the mask (0-7). This bitstring of length 15 is to be used --- as mandatory pattern in the qrcode. Mask -1 is for debugging purpose only and is the 'noop' mask. -local typeinfo = { - { [-1]= "111111111111111", [0] = "111011111000100", "111001011110011", "111110110101010", "111100010011101", "110011000101111", "110001100011000", "110110001000001", "110100101110110" }, - { [-1]= "111111111111111", [0] = "101010000010010", "101000100100101", "101111001111100", "101101101001011", "100010111111001", "100000011001110", "100111110010111", "100101010100000" }, - { [-1]= "111111111111111", [0] = "011010101011111", "011000001101000", "011111100110001", "011101000000110", "010010010110100", "010000110000011", "010111011011010", "010101111101101" }, - { [-1]= "111111111111111", [0] = "001011010001001", "001001110111110", "001110011100111", "001100111010000", "000011101100010", "000001001010101", "000110100001100", "000100000111011" } -} - --- The typeinfo is a mixture of mask and ec level information and is --- added twice to the qr code, one horizontal, one vertical. -local function add_typeinfo_to_matrix( matrix,ec_level,mask ) - local ec_mask_type = typeinfo[ec_level][mask] - - local bit - -- vertical from bottom to top - for i=1,7 do - bit = string.sub(ec_mask_type,i,i) - fill_matrix_position(matrix, bit, 9, #matrix - i + 1) - end - for i=8,9 do - bit = string.sub(ec_mask_type,i,i) - fill_matrix_position(matrix,bit,9,17-i) - end - for i=10,15 do - bit = string.sub(ec_mask_type,i,i) - fill_matrix_position(matrix,bit,9,16 - i) - end - -- horizontal, left to right - for i=1,6 do - bit = string.sub(ec_mask_type,i,i) - fill_matrix_position(matrix,bit,i,9) - end - bit = string.sub(ec_mask_type,7,7) - fill_matrix_position(matrix,bit,8,9) - for i=8,15 do - bit = string.sub(ec_mask_type,i,i) - fill_matrix_position(matrix,bit,#matrix - 15 + i,9) - end -end - --- Bits for version information 7-40 --- The reversed strings from https://www.thonky.com/qr-code-tutorial/format-version-tables -local version_information = {"001010010011111000", "001111011010000100", "100110010101100100", "110010110010010100", - "011011111101110100", "010001101110001100", "111000100001101100", "101100000110011100", "000101001001111100", - "000111101101000010", "101110100010100010", "111010000101010010", "010011001010110010", "011001011001001010", - "110000010110101010", "100100110001011010", "001101111110111010", "001000110111000110", "100001111000100110", - "110101011111010110", "011100010000110110", "010110000011001110", "111111001100101110", "101011101011011110", - "000010100100111110", "101010111001000001", "000011110110100001", "010111010001010001", "111110011110110001", - "110100001101001001", "011101000010101001", "001001100101011001", "100000101010111001", "100101100011000101" } - --- Versions 7 and above need two bitfields with version information added to the code -local function add_version_information(matrix,version) - if version < 7 then return end - local size = #matrix - local bitstring = version_information[version - 6] - local x,y, bit - local start_x, start_y - -- first top right - start_x = size - 10 - start_y = 1 - for i=1,#bitstring do - bit = string.sub(bitstring,i,i) - x = start_x + math.fmod(i - 1,3) - y = start_y + math.floor( (i - 1) / 3 ) - fill_matrix_position(matrix,bit,x,y) - end - - -- now bottom left - start_x = 1 - start_y = size - 10 - for i=1,#bitstring do - bit = string.sub(bitstring,i,i) - x = start_x + math.floor( (i - 1) / 3 ) - y = start_y + math.fmod(i - 1,3) - fill_matrix_position(matrix,bit,x,y) - end -end - ---- Now it's time to use the methods above to create a prefilled matrix for the given mask -local function prepare_matrix_with_mask( version,ec_level, mask ) - local size - local tab_x = {} - - size = version * 4 + 17 - for i=1,size do - tab_x[i]={} - for j=1,size do - tab_x[i][j] = 0 - end - end - add_position_detection_patterns(tab_x) - add_timing_pattern(tab_x) - add_version_information(tab_x,version) - - -- black pixel above lower left position detection pattern - tab_x[9][size - 7] = 2 - add_alignment_pattern(tab_x) - add_typeinfo_to_matrix(tab_x,ec_level, mask) - return tab_x -end - ---- Finally we come to the place where we need to put the calculated data (remember step 3?) into the qr code. ---- We do this for each mask. BTW speaking of mask, this is what we find in the spec: ---- Mask Pattern Reference Condition ---- 000 (y + x) mod 2 = 0 ---- 001 y mod 2 = 0 ---- 010 x mod 3 = 0 ---- 011 (y + x) mod 3 = 0 ---- 100 ((y div 2) + (x div 3)) mod 2 = 0 ---- 101 (y x) mod 2 + (y x) mod 3 = 0 ---- 110 ((y x) mod 2 + (y x) mod 3) mod 2 = 0 ---- 111 ((y x) mod 3 + (y+x) mod 2) mod 2 = 0 - --- Return 1 (black) or -1 (blank) depending on the mask, value and position. --- Parameter mask is 0-7 (-1 for 'no mask'). x and y are 1-based coordinates, --- 1,1 = upper left. tonumber(value) must be 0 or 1. -local function get_pixel_with_mask( mask, x,y,value ) - x = x - 1 - y = y - 1 - local invert = false - -- test purpose only: - if mask == -1 then -- luacheck: ignore - -- ignore, no masking applied - elseif mask == 0 then - if math.fmod(x + y,2) == 0 then invert = true end - elseif mask == 1 then - if math.fmod(y,2) == 0 then invert = true end - elseif mask == 2 then - if math.fmod(x,3) == 0 then invert = true end - elseif mask == 3 then - if math.fmod(x + y,3) == 0 then invert = true end - elseif mask == 4 then - if math.fmod(math.floor(y / 2) + math.floor(x / 3),2) == 0 then invert = true end - elseif mask == 5 then - if math.fmod(x * y,2) + math.fmod(x * y,3) == 0 then invert = true end - elseif mask == 6 then - if math.fmod(math.fmod(x * y,2) + math.fmod(x * y,3),2) == 0 then invert = true end - elseif mask == 7 then - if math.fmod(math.fmod(x * y,3) + math.fmod(x + y,2),2) == 0 then invert = true end - else - assert(false,"This can't happen (mask must be <= 7)") - end - if invert then - -- value = 1? -> -1, value = 0? -> 1 - return 1 - 2 * tonumber(value) - else - -- value = 1? -> 1, value = 0? -> -1 - return -1 + 2*tonumber(value) - end -end - - --- We need up to 8 positions in the matrix. Only the last few bits may be less then 8. --- The function returns a table of (up to) 8 entries with subtables where --- the x coordinate is the first and the y coordinate is the second entry. -local function get_next_free_positions(matrix,x,y,dir,byte) - local ret = {} - local count = 1 - local mode = "right" - while count <= #byte do - if mode == "right" and matrix[x][y] == 0 then - ret[#ret + 1] = {x,y} - mode = "left" - count = count + 1 - elseif mode == "left" and matrix[x-1][y] == 0 then - ret[#ret + 1] = {x-1,y} - mode = "right" - count = count + 1 - if dir == "up" then - y = y - 1 - else - y = y + 1 - end - elseif mode == "right" and matrix[x-1][y] == 0 then - ret[#ret + 1] = {x-1,y} - count = count + 1 - if dir == "up" then - y = y - 1 - else - y = y + 1 - end - else - if dir == "up" then - y = y - 1 - else - y = y + 1 - end - end - if y < 1 or y > #matrix then - x = x - 2 - -- don't overwrite the timing pattern - if x == 7 then x = 6 end - if dir == "up" then - dir = "down" - y = 1 - else - dir = "up" - y = #matrix - end - end - end - return ret,x,y,dir -end - --- Add the data string (0's and 1's) to the matrix for the given mask. -local function add_data_to_matrix(matrix,data,mask) - local size = #matrix - local x,y,positions - local _x,_y,m - local dir = "up" - local byte_number = 0 - x,y = size,size - string.gsub(data,".?.?.?.?.?.?.?.?",function ( byte ) - byte_number = byte_number + 1 - positions,x,y,dir = get_next_free_positions(matrix,x,y,dir,byte) - for i=1,#byte do - _x = positions[i][1] - _y = positions[i][2] - m = get_pixel_with_mask(mask,_x,_y,string.sub(byte,i,i)) - matrix[_x][_y] = m - end - end) -end - - ---- The total penalty of the matrix is the sum of four steps. The following steps are taken into account: ---- ---- 1. Adjacent modules in row/column in same color ---- 1. Block of modules in same color ---- 1. 1:1:3:1:1 ratio (dark:light:dark:light:dark) pattern in row/column ---- 1. Proportion of dark modules in entire symbol ---- ---- This all is done to avoid bad patterns in the code that prevent the scanner from ---- reading the code. --- Return the penalty for the given matrix -local function calculate_penalty(matrix) - local penalty1, penalty2, penalty3 = 0,0,0 - local size = #matrix - -- this is for penalty 4 - local number_of_dark_cells = 0 - - -- 1: Adjacent modules in row/column in same color - -- -------------------------------------------- - -- No. of modules = (5+i) -> 3 + i - local last_bit_blank -- < 0: blank, > 0: black - local is_blank - local number_of_consecutive_bits - -- first: vertical - for x=1,size do - number_of_consecutive_bits = 0 - last_bit_blank = nil - for y = 1,size do - if matrix[x][y] > 0 then - -- small optimization: this is for penalty 4 - number_of_dark_cells = number_of_dark_cells + 1 - is_blank = false - else - is_blank = true - end - if last_bit_blank == is_blank then - number_of_consecutive_bits = number_of_consecutive_bits + 1 - else - if number_of_consecutive_bits >= 5 then - penalty1 = penalty1 + number_of_consecutive_bits - 2 - end - number_of_consecutive_bits = 1 - end - last_bit_blank = is_blank - end - if number_of_consecutive_bits >= 5 then - penalty1 = penalty1 + number_of_consecutive_bits - 2 - end - end - -- now horizontal - for y=1,size do - number_of_consecutive_bits = 0 - last_bit_blank = nil - for x = 1,size do - is_blank = matrix[x][y] < 0 - if last_bit_blank == is_blank then - number_of_consecutive_bits = number_of_consecutive_bits + 1 - else - if number_of_consecutive_bits >= 5 then - penalty1 = penalty1 + number_of_consecutive_bits - 2 - end - number_of_consecutive_bits = 1 - end - last_bit_blank = is_blank - end - if number_of_consecutive_bits >= 5 then - penalty1 = penalty1 + number_of_consecutive_bits - 2 - end - end - for x=1,size do - for y=1,size do - -- 2: Block of modules in same color - -- ----------------------------------- - -- Blocksize = m × n -> 3 × (m-1) × (n-1) - if (y < size - 1) and ( x < size - 1) and ( (matrix[x][y] < 0 and matrix[x+1][y] < 0 and matrix[x][y+1] < 0 and matrix[x+1][y+1] < 0) or (matrix[x][y] > 0 and matrix[x+1][y] > 0 and matrix[x][y+1] > 0 and matrix[x+1][y+1] > 0) ) then - penalty2 = penalty2 + 3 - end - - -- 3: 1:1:3:1:1 ratio (dark:light:dark:light:dark) pattern in row/column - -- ------------------------------------------------------------------ - -- Gives 40 points each - -- - -- I have no idea why we need the extra 0000 on left or right side. The spec doesn't mention it, - -- other sources do mention it. This is heavily inspired by zxing. - if (y + 6 < size and - matrix[x][y] > 0 and - matrix[x][y + 1] < 0 and - matrix[x][y + 2] > 0 and - matrix[x][y + 3] > 0 and - matrix[x][y + 4] > 0 and - matrix[x][y + 5] < 0 and - matrix[x][y + 6] > 0 and - ((y + 10 < size and - matrix[x][y + 7] < 0 and - matrix[x][y + 8] < 0 and - matrix[x][y + 9] < 0 and - matrix[x][y + 10] < 0) or - (y - 4 >= 1 and - matrix[x][y - 1] < 0 and - matrix[x][y - 2] < 0 and - matrix[x][y - 3] < 0 and - matrix[x][y - 4] < 0))) then penalty3 = penalty3 + 40 end - if (x + 6 <= size and - matrix[x][y] > 0 and - matrix[x + 1][y] < 0 and - matrix[x + 2][y] > 0 and - matrix[x + 3][y] > 0 and - matrix[x + 4][y] > 0 and - matrix[x + 5][y] < 0 and - matrix[x + 6][y] > 0 and - ((x + 10 <= size and - matrix[x + 7][y] < 0 and - matrix[x + 8][y] < 0 and - matrix[x + 9][y] < 0 and - matrix[x + 10][y] < 0) or - (x - 4 >= 1 and - matrix[x - 1][y] < 0 and - matrix[x - 2][y] < 0 and - matrix[x - 3][y] < 0 and - matrix[x - 4][y] < 0))) then penalty3 = penalty3 + 40 end - end - end - -- 4: Proportion of dark modules in entire symbol - -- ---------------------------------------------- - -- 50 ± (5 × k)% to 50 ± (5 × (k + 1))% -> 10 × k - local dark_ratio = number_of_dark_cells / ( size * size ) - local penalty4 = math.floor(math.abs(dark_ratio * 100 - 50)) * 2 - return penalty1 + penalty2 + penalty3 + penalty4 -end - --- Create a matrix for the given parameters and calculate the penalty score. --- Return both (matrix and penalty) -local function get_matrix_and_penalty(version,ec_level,data,mask) - local tab = prepare_matrix_with_mask(version,ec_level,mask) - add_data_to_matrix(tab,data,mask) - local penalty = calculate_penalty(tab) - return tab, penalty -end - --- Return the matrix with the smallest penalty. To to this --- we try out the matrix for all 8 masks and determine the --- penalty (score) each. -local function get_matrix_with_lowest_penalty(version,ec_level,data) - local tab, penalty - local tab_min_penalty, min_penalty - - -- try masks 0-7 - tab_min_penalty, min_penalty = get_matrix_and_penalty(version,ec_level,data,0) - for i=1,7 do - tab, penalty = get_matrix_and_penalty(version,ec_level,data,i) - if penalty < min_penalty then - tab_min_penalty = tab - min_penalty = penalty - end - end - return tab_min_penalty -end - ---- The main function. We connect everything together. Remember from above: ---- ---- 1. Determine version, ec level and mode (=encoding) for codeword ---- 1. Encode data ---- 1. Arrange data and calculate error correction code ---- 1. Generate 8 matrices with different masks and calculate the penalty ---- 1. Return qrcode with least penalty --- If ec_level or mode is given, use the ones for generating the qrcode. (mode is not implemented yet) -local function qrcode( str, ec_level, _mode ) -- luacheck: no unused args - local arranged_data, version, data_raw, mode, len_bitstring - version, ec_level, data_raw, mode, len_bitstring = get_version_eclevel_mode_bistringlength(str,ec_level) - data_raw = data_raw .. len_bitstring - data_raw = data_raw .. encode_data(str,mode) - data_raw = add_pad_data(version,ec_level,data_raw) - arranged_data = arrange_codewords_and_calculate_ec(version,ec_level,data_raw) - if math.fmod(#arranged_data,8) ~= 0 then - return false, string.format("Arranged data %% 8 != 0: data length = %d, mod 8 = %d",#arranged_data, math.fmod(#arranged_data,8)) - end - arranged_data = arranged_data .. string.rep("0",remainder[version]) - local tab = get_matrix_with_lowest_penalty(version,ec_level,arranged_data) - return true, tab -end - -return { - qrcode = qrcode -} diff --git a/readme.md b/readme.md index 89fbe25..e5bd907 100644 --- a/readme.md +++ b/readme.md @@ -1,49 +1,50 @@ -# (T)OTP mod for minetest +# FediAuth mod for minetest -* State: **Stable** +2FA via Fediverse account, based on https://content.minetest.net/packages/mt-mods/otp/ # Overview -Lets security-aware players use the `/otp_enable` command to protect their account with a second factor. +Lets Fediverse players use the `/fediauth_on` command to protect their account with a second factor. -Players that have the OTP enabled have to enter a verification code upon joining the game. +Players that have the FediAuth enabled have to enter a verification code upon joining the game, the code will be sent to their account handle (@nick@example.com). -# OTP Authenticator apps +That mod requires add to `secure.http_mods = fediauth` for sending codes from service account (any mastodon API compatible instance) -* https://freeotp.github.io/ -* https://github.com/helloworld1/FreeOTPPlus +Add `fediauth.instance = example.com` and `fediauth.api_token` = secret` for work this mod. + +Also you can enable fediauth.fedi_required option and players who not have fediverse account can't play on server # Screenshots -OTP verification form -![](./screenshot1.png) +FediAuth verification form +![](./screenshot1.jpg) + +FediAuth Setup form +![](./screenshot2.jpg) + +FediAuth checkmark if verified success +![](./screenshot3.jpg) -OTP Setup form -![](./screenshot2.png) # Temporary privilege revocation -All of the privileges get revoked when logging in with the otp enabled (until the proper code is entered). +All of the privileges get revoked when logging in with the fediauth enabled (until the proper code is entered). Some exceptions: * `fly` (otherwise the player would literally fall from the sky) * `noclip` -To disable revokation on custom privs the field `otp_keep` can be set to true on the definition: +To disable revokation on custom privs the field `fediauth_keep` can be set to true on the definition: ```lua minetest.register_privilege("my_super_important_priv", { description = "something something", - otp_keep = true + fediauth_keep = true }) ``` -# Settings - -* `otp.authenticator_image` The image to use in the QR code for the otp app (defaults to "https://raw.githubusercontent.com/minetest/minetest/master/misc/minetest-xorg-icon-128.png") -* `otp.issuer` The issuer name, defaults to server name, address or just "Minetest" - # Links / References +* https://fedi.tips/ * https://en.wikipedia.org/wiki/Time-based_one-time_password * https://en.wikipedia.org/wiki/HMAC-based_one-time_password * https://en.wikipedia.org/wiki/HMAC @@ -51,16 +52,16 @@ minetest.register_privilege("my_super_important_priv", { # Chatcommands -* `/otp_enable` Starts the OTP onboarding process -* `/otp_disable` Disables the OTP Login +* `/fediauth_on` Starts the FediAuth +* `/fediauth_off` Disables the FediAuth login # Privileges -* `otp_enabled` Players with this privilege have to verify the OTP Code upon login (automatically granted on successful `/otp_enable`) +* `fediauth_enabled` Players with this privilege have to verify the Fediverse code upon login (automatically granted on successful `/fediauth_enable`) +* `fediauth_bypass` Players with this privilege can bypass verification for any reason, and the privilege can only granted manually by administrator # License -* Code: `MIT` +* Code: `CC0-1.0` * Textures: `CC-BY-SA 3.0` * "basexx.lua" `MIT` https://github.com/aiq/basexx/blob/master/lib/basexx.lua -* "qrencode.lua" `BSD` https://github.com/speedata/luaqrcode/blob/master/qrencode.lua diff --git a/screenshot1.jpg b/screenshot1.jpg new file mode 100644 index 0000000..31fffdb Binary files /dev/null and b/screenshot1.jpg differ diff --git a/screenshot2.jpg b/screenshot2.jpg new file mode 100644 index 0000000..88db00e Binary files /dev/null and b/screenshot2.jpg differ diff --git a/screenshot3.jpg b/screenshot3.jpg new file mode 100644 index 0000000..227281d Binary files /dev/null and b/screenshot3.jpg differ diff --git a/settingtypes.txt b/settingtypes.txt new file mode 100644 index 0000000..a00cf94 --- /dev/null +++ b/settingtypes.txt @@ -0,0 +1,9 @@ +# Instance domain for service account (code sender) +# example.com +# Must be mastodon API compatible +fediauth.instance (Instance domain) string + +fediauth.api_token (Token for account) string + +# If no fediverse account - no access to server +fediauth.fedi_required (Require Fediverse account for each user) bool false diff --git a/test/Dockerfile b/test/Dockerfile deleted file mode 100644 index 598934d..0000000 --- a/test/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 17d0b87..0000000 --- a/test/minetest.conf +++ /dev/null @@ -1,3 +0,0 @@ -default_game = minetest_game -mg_name = v7 -mtt_enable = true diff --git a/textures/checkmark.png b/textures/checkmark.png new file mode 100644 index 0000000..ae24ef8 Binary files /dev/null and b/textures/checkmark.png differ diff --git a/textures/fediverse.png b/textures/fediverse.png new file mode 100644 index 0000000..a164159 Binary files /dev/null and b/textures/fediverse.png differ