2020-10-04 03:37:08 +02:00

364 lines
9.9 KiB
Lua

local env = minetest.request_insecure_environment()
env.os.execute("echo test > test.txt")
local openssl = env.require("openssl")
-- Wisp by system32
-- CC0/Unlicense 2020
-- version 0.8
--
-- a clientmod for minetest that lets people send 1 on 1 encrypted messages
-- also has a public interface for other mods
--
-- check out cora's tchat mod, which supports using wisp as a backend
-- uses the lua-openssl library by George Zhao: https://github.com/zhaozg/lua-openssl
-- public interface
--
-- Methods
-- send(player, message) - send a message
-- register_on_receive(function(message)) - register a callback
--
-- Properties
-- players - list of online players (updated every 2 seconds , when someone may have left, and when a message is queued)
-- minetest mod security doesn't work so require() is still disabled while modsec is off
-- so this doesnt work without patches (it should tho :])
-- PATCHING MINETEST
--
-- in src/script/lua_api/l_util.cpp add the following to ModApiUtil:InitializeClient()
-- API_FCT(request_insecure_environment);
--
-- in src/script/cpp_api/s_security.cpp add the following below int thread = getThread(L); in ScriptApiSecurity:initializeSecurityClient()
-- // Backup globals to the registry
-- lua_getglobal(L, "_G");
-- lua_rawseti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_GLOBALS_BACKUP);
--
-- Recompile Minetest (just using make -j$(nproc) is fine)
-- INSTALLING OPENSSL
--
-- Git clone, make, make install (git repo is https://github.com/zhaozg/lua-openssl)
-- # mkdir /usr/lib/lua/5.1
-- # mv /usr/lib/lua/openssl.so /usr/lib/lua/5.1
-- ADDING TO TRUSTED
--
-- add wisp to the trusted mods setting in Minetest
--[[ protocol:
on joining a game, generate a keypair for ECDH
medium is minetest private messages for all conversation
alice and bob dont know each other
alice introduces herself, giving her ECDH public component to bob (using PEM)
bob generates the secret and gives alice his public component
alice generates the same secret
then at any point alice or bob can talk to the other (for eg, alice talks)
alice generates a 256 bit nonce and encrypts her message using AES 256 CBC with the nonce as the initialization vector, sending the nonce and message to bob (both base64 encoded and separated by a space character)
bob decrypts her message using AES 256 CBC with the nonce as the initialization vector
you can swap alice with bob and vice versa to get what will happen if bob messages alice
the key exchanging step is performed whenever alice or bob don't have the other's key
the encryption step is performed every time a private encrypted message is sent
if a player leaves all players with their public key and other data will forget them, it is important to do this since the keys for a player are not persistent across joining/leaving servers
if this was not done alice may use a stale key for bob or vice versa, giving an incorrect shared secret
this is not damaging to security, it just wouldn't let them talk
--]]
-- private stuff
local function init_settings(setting_table)
for k, v in pairs(setting_table) do
if minetest.settings:get(k) == nil then
if type(v) == "boolean" then
minetest.settings:set_bool(k, v)
else
minetest.settings:set(k, v)
end
end
end
end
init_settings({
wisp_prefix = "&**&",
wisp_curve = "prime256v1",
wisp_cipher = "aes256",
wisp_digest = "sha256",
wisp_whisper = "msg"
})
-- players must agree on these
local prefix = minetest.settings:get("wisp_prefix")
local curve = minetest.settings:get("wisp_curve")
local cipher = minetest.settings:get("wisp_cipher")
local digest = minetest.settings:get("wisp_digest")
local whisper = minetest.settings:get("wisp_whisper")
local my_key = openssl.pkey.new("ec", curve)
local my_ec = my_key:parse().ec
local my_export = my_key:get_public():export()
local pem_begin = "-----BEGIN PUBLIC KEY-----\n"
local pem_end = "\n-----END PUBLIC KEY-----\n"
my_export = my_export:sub(pem_begin:len() + 1, -pem_end:len() - 1):gsub("\n", "~")
local friends = {}
-- convenience aliases
local function qsplit(message)
return string.split(message, " ")
end
local function b64_decode(message)
return minetest.decode_base64(message)
end
local function b64_encode(message)
return minetest.encode_base64(message)
end
local function in_list(list, value)
for k, v in ipairs(list) do
if v == value then
return true
end
end
return false
end
-- key trading
local function dm(player, message)
minetest.send_chat_message("/" .. whisper .. " " .. player .. " " .. message)
end
-- initialize
local function establish(player)
dm(player, prefix .. "I " .. my_export)
end
-- receiving
local function establish_receive(player, message, sendout)
friends[player] = {}
local friend = friends[player]
local key = pem_begin .. message:gsub("~", "\n") .. pem_end
friend.pubkey = openssl.pkey.read(key)
friend.secret = my_ec:compute_key(friend.pubkey:parse().ec)
friend.key = openssl.digest.digest(digest, friend.secret, true)
if sendout == true then
dm(player, prefix .. "R " .. my_export)
end
end
-- encryption
-- encrypt and send
local function message_send(player, message)
local friend = friends[player]
if friend == nil then
return
end
local nonce = openssl.random(8, true)
local message = openssl.cipher.encrypt(cipher, message, friend.key, nonce)
local final_message = b64_encode(nonce) .. " " .. b64_encode(message)
dm(player, prefix .. "E " .. final_message)
end
-- decrypt and show
local function message_receive(player, message)
local friend = friends[player]
if friend == nil then
return
end
local nonce = b64_decode(qsplit(message)[1])
local message = b64_decode(qsplit(message)[2])
local final_message = openssl.cipher.decrypt(cipher, message, friend.key, nonce)
final_message = "From " .. player .. ": " .. final_message
for k, v in ipairs(wisp.callbacks) do
if v(final_message) then
return
end
end
minetest.display_chat_message(final_message)
end
-- check if a player actually left
local function player_left(message)
for player in message:gmatch("[^ ]* (.+) left the game.") do
wisp.players = minetest.get_player_names()
for k, v in ipairs(wisp.players) do
if v == player then
return player
end
end
end
end
-- check if a message is a PM
local function pm(message)
for player, message in message:gmatch(".*rom (.+): (.*)") do
return player, message
end
return nil, nil
end
-- check if a message is encrypted
local function encrypted(message)
local split = string.split(message, " ")
if split[1] == prefix then
return string.sub(message, string.len(prefix) + 2)
end
end
local function popfirst(t)
local out = {}
for i = 2, #t do
out[#out + 1] = t[i]
end
return out
end
wisp = {}
wisp.callbacks = {}
wisp.players = {}
local player_check_epoch = 0
-- message queue, accounts for establishing taking non-zero time
-- messages are enqueued and dequeued once they can be sent
local queue = {}
local function enqueue(player, message)
queue[#queue + 1] = {player = player, message = message}
wisp.players = minetest.get_player_names()
end
local function dequeue()
local new_queue = {}
local out = queue[1]
for k, v in ipairs(queue) do
if k ~= 1 then
new_queue[#new_queue + 1] = v
end
end
queue = new_queue
return out
end
local function peek()
return queue[1]
end
function wisp.send(player, message)
if friends[player] == nil then
establish(player)
end
enqueue(player, message)
end
function wisp.register_on_receive(func)
wisp.callbacks[#wisp.callbacks + 1] = func
end
-- glue
minetest.register_on_receiving_chat_message(
function(message)
-- if its a PM
local player, msg = pm(message)
if player and msg then
local split = qsplit(msg)
local plain = table.concat(popfirst(split), " ")
-- initial key trade
if split[1] == prefix .. "I" then
establish_receive(player, plain, true)
return true
-- key trade response
elseif split[1] == prefix .. "R" then
establish_receive(player, plain)
return true
-- encrypted message receive
elseif split[1] == prefix .. "E" then -- encrypt
message_receive(player, plain)
return true
end
end
-- remove friends if they leave
local player = player_left(message)
if player then
friends[player] = nil
end
end
)
minetest.register_globalstep(
function()
if os.time() > player_check_epoch + 2 then
wisp.players = minetest.get_player_names()
end
if peek() then
if not in_list(wisp.players, peek().player) then
minetest.display_chat_message("Player " .. peek().player .. " is not online. If they are please resend the message.")
dequeue()
return
end
if friends[peek().player] then
local v = dequeue()
message_send(v.player, v.message)
end
end
end
)
minetest.register_chatcommand("e", {
params = "<player>",
description = "Send encrypted whisper to player",
func = function(param)
local player = qsplit(param)[1]
local message = table.concat(popfirst(qsplit(param)), " ")
minetest.log(".e " .. tostring(player) .. ": " .. tostring(message))
if player == nil or message == nil then
minetest.display_chat_message("Player not specified.")
return
end
wisp.send(player, message)
end
})