incrementaltp: respect physics overrides
[waspsaliva.git] / clientmods / wisp / init.lua
blobdd61c46c14db8c4c110a6568d9daf26b02d0a2e4
1 -- Wisp by system32
2 -- CC0/Unlicense 2020
3 -- version 1.0
4 --
5 -- a clientmod for minetest that lets people send 1 on 1 encrypted messages
6 -- also has a public interface for other mods
7 --
8 -- check out cora's tchat mod, which supports using wisp as a backend
10 -- uses the lua-openssl library by George Zhao: https://github.com/zhaozg/lua-openssl
12 -- public interface
14 -- Methods
15 -- send(player, message) - send a message
16 -- register_on_receive(function(message)) - register a receiving callback (includes To: messages), if it returns true the message will not be shown to the player
17 -- register_on_receive_split(function(player, message)) - register_on_receive but player and message are pre split
18 -- register_on_send_split(function(player, message)) - register a sending callback, if it returns true the message will not be sent
20 -- Properties
21 -- players - list of online players (updated every 2 seconds , when someone may have left, and when a message is queued)
23 -- minetest mod security doesn't work so require() is still disabled while modsec is off
24 -- so this doesnt work without patches (it should tho :])
26 -- PATCHING MINETEST
28 -- in src/script/lua_api/l_util.cpp add the following to ModApiUtil:InitializeClient() below API_FCT(decompress);
29 --[[
30 API_FCT(request_insecure_environment);
31 --]]
33 -- in src/script/cpp_api/s_security.cpp add the following below int thread = getThread(L); in ScriptApiSecurity:initializeSecurityClient()
34 --[[
35 // Backup globals to the registry
36 lua_getglobal(L, "_G");
37 lua_rawseti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_GLOBALS_BACKUP);
38 --]]
40 -- Recompile Minetest (just using make -j$(nproc) is fine)
42 -- INSTALLING OPENSSL
44 -- Git clone, make, make install (git repo is https://github.com/zhaozg/lua-openssl)
45 -- # mkdir /usr/lib/lua/5.1
46 -- # mv /usr/lib/lua/openssl.so /usr/lib/lua/5.1
48 -- ADDING TO TRUSTED
50 -- add wisp to the trusted mods setting in Minetest
52 --[[ protocol:
53 on joining a game, generate a keypair for ECDH
55 medium is minetest private messages for all conversation
57 alice and bob dont know each other
58 alice introduces herself, giving her ECDH public component to bob (using PEM)
59 bob generates the secret and gives alice his public component
60 alice generates the same secret
62 then at any point alice or bob can talk to the other (for eg, alice talks)
63 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)
64 bob decrypts her message using AES 256 CBC with the nonce as the initialization vector
65 you can swap alice with bob and vice versa to get what will happen if bob messages alice
67 the key exchanging step is performed whenever alice or bob don't have the other's key
68 the encryption step is performed every time a private encrypted message is sent
70 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
71 if this was not done alice may use a stale key for bob or vice versa, giving an incorrect shared secret
72 this is not damaging to security, it just wouldn't let them talk
73 --]]
76 if minetest.request_insecure_environment == nil then
77 error("Wisp: Minetest scripting patches were not applied, please apply them and recompile Minetest.")
78 end
80 local env = minetest.request_insecure_environment()
81 if env == nil then
82 error("Wisp: not in trusted mods (secure.trusted_mods), please go into the advanced settings and add wisp (all lowercase).")
83 end
85 local openssl = env.require("openssl")
88 -- private stuff
90 local function init_settings(setting_table)
91 for k, v in pairs(setting_table) do
92 if minetest.settings:get(k) == nil then
93 if type(v) == "boolean" then
94 minetest.settings:set_bool(k, v)
95 else
96 minetest.settings:set(k, v)
97 end
98 end
99 end
102 init_settings({
103 wisp_prefix = "&**&",
104 wisp_curve = "prime256v1",
105 wisp_cipher = "aes256",
106 wisp_digest = "sha256",
107 wisp_iv_size = 8,
108 wisp_whisper = "msg",
109 wisp_hide_sent = true,
110 wisp_timeout = 10
113 -- players must agree on these
114 local prefix = minetest.settings:get("wisp_prefix")
115 local curve = minetest.settings:get("wisp_curve")
116 local cipher = minetest.settings:get("wisp_cipher")
117 local digest = minetest.settings:get("wisp_digest")
119 local iv_size = minetest.settings:get("wisp_iv_size")
120 local whisper = minetest.settings:get("wisp_whisper")
121 local hide_sent = minetest.settings:get_bool("wisp_hide_sent")
123 local timeout = tonumber(minetest.settings:get("wisp_timeout"))
125 local my_key = openssl.pkey.new("ec", curve)
126 local my_ec = my_key:parse().ec
127 local my_export = my_key:get_public():export()
129 local pem_begin = "-----BEGIN PUBLIC KEY-----\n"
130 local pem_end = "\n-----END PUBLIC KEY-----\n"
132 my_export = my_export:sub(pem_begin:len() + 1, -pem_end:len() - 1):gsub("\n", "~")
134 local friends = {}
137 -- convenience aliases
138 local function qsplit(message)
139 return string.split(message, " ")
142 local function b64_decode(message)
143 return minetest.decode_base64(message)
146 local function b64_encode(message)
147 return minetest.encode_base64(message)
150 local function in_list(list, value)
151 for k, v in ipairs(list) do
152 if v == value then
153 return true
156 return false
159 local function append(list, item)
160 list[#list + 1] = item
163 local function popfirst(t)
164 local out = {}
166 for i = 2, #t do
167 out[#out + 1] = t[i]
170 return out
173 local function unpack(t, i)
174 if type(t) ~= "table" then
175 return t
178 i = i or 1
179 if t[i] ~= nil then
180 return t[i], unpack(t, i + 1)
185 -- key trading
187 local function dm(player, message)
188 minetest.send_chat_message("/" .. whisper .. " " .. player .. " " .. message)
191 -- initialize
192 local function establish(player)
193 dm(player, prefix .. "I " .. my_export)
196 -- receiving
197 local function establish_receive(player, message, sendout)
198 friends[player] = {}
199 local friend = friends[player]
201 local key = pem_begin .. message:gsub("~", "\n") .. pem_end
203 friend.pubkey = openssl.pkey.read(key)
205 friend.secret = my_ec:compute_key(friend.pubkey:parse().ec)
206 friend.key = openssl.digest.digest(digest, friend.secret, true)
208 if sendout == true then
209 dm(player, prefix .. "R " .. my_export)
214 -- encryption
216 local function run_callbacks(list, params)
217 for k, v in ipairs(list) do
218 if v(unpack(params)) then
219 return true
224 -- encrypt and send
225 local function message_send(player, message, hide_to, force_send)
226 local me = minetest.localplayer:get_name()
228 if run_callbacks(wisp.send_split_callbacks, {player, message}) then
229 return
232 -- for displaying the To: stuff
233 if not hide_to then
234 local target = player
235 if target == me then
236 target = "Yourself"
238 local display_message = "To " .. target .. ": " .. message
240 local callback_value = run_callbacks(wisp.receive_callbacks, display_message)
241 callback_value = callback_value or run_callbacks(wisp.receive_split_callbacks, {player, message})
243 if not callback_value then
244 minetest.display_chat_message(display_message)
248 -- actual encryption
249 local friend = friends[player]
250 if friend == nil then
251 return
254 local nonce = openssl.random(iv_size, true)
255 local enc_message = openssl.cipher.encrypt(cipher, message, friend.key, nonce)
256 local final_message = b64_encode(nonce) .. " " .. b64_encode(enc_message)
258 if player ~= me or force_send then
259 dm(player, prefix .. "E " .. final_message)
263 -- decrypt and show
264 local function message_receive(player, message)
265 local friend = friends[player]
266 if friend == nil then
267 return
270 local nonce = b64_decode(qsplit(message)[1])
271 local enc_message = b64_decode(qsplit(message)[2])
272 local dec_message = openssl.cipher.decrypt(cipher, enc_message, friend.key, nonce)
273 final_message = "From " .. player .. ": " .. dec_message
275 local callback_value = run_callbacks(wisp.receive_callbacks, final_message)
276 callback_value = callback_value or run_callbacks(wisp.receive_split_callbacks, {player, dec_message})
278 if not callback_value then
279 minetest.display_chat_message(final_message)
284 -- check if a player actually left
285 local function player_left(message)
286 for player in message:gmatch("[^ ]* (.+) left the game.") do
287 wisp.players = minetest.get_player_names()
288 for k, v in ipairs(wisp.players) do
289 if v == player then
290 return player
296 -- check if a message is a PM
297 local function pm(message)
298 for player, message in message:gmatch(".*rom (.+): (.*)") do
299 return player, message
302 return nil, nil
305 -- check if a message is encrypted
306 local function encrypted(message)
307 local split = string.split(message, " ")
309 if split[1] == prefix then
310 return string.sub(message, string.len(prefix) + 2)
314 -- check if a message is 'Message sent.' or similar
315 local function message_sent(message)
316 return message == "Message sent."
321 wisp = {}
322 wisp.receive_callbacks = {}
323 wisp.receive_split_callbacks = {}
324 wisp.send_split_callbacks = {}
325 wisp.players = {}
328 local player_check_epoch = 0
330 -- message queue, accounts for establishing taking non-zero time
331 -- messages are enqueued and dequeued once they can be sent
332 local queue = {}
334 local function enqueue(player, message, hide_to, force_send)
335 append(queue, {
336 player = player,
337 message = message,
338 hide_to = hide_to,
339 force_send = force_send,
340 time = os.time()
342 wisp.players = minetest.get_player_names()
345 local function dequeue()
346 local new_queue = {}
347 local out = queue[1]
348 for k, v in ipairs(queue) do
349 if k ~= 1 then
350 append(new_queue, v)
353 queue = new_queue
354 return out
357 local function peek()
358 return queue[1]
362 function wisp.send(player, message, hide_to, force_send)
363 if (player ~= minetest.localplayer:get_name() or force_send) and friends[player] == nil then
364 establish(player)
366 enqueue(player, message, hide_to, force_send)
369 function wisp.register_on_receive(func)
370 append(wisp.receive_callbacks, func)
373 function wisp.register_on_receive_split(func)
374 append(wisp.receive_split_callbacks, func)
377 function wisp.register_on_send_split(func)
378 append(wisp.send_split_callbacks, func)
382 -- glue
384 minetest.register_on_receiving_chat_message(
385 function(message)
386 -- hide Message sent.
387 if hide_sent and message_sent(message) then
388 return true
391 -- if its a PM
392 local player, msg = pm(message)
393 if player and msg then
395 local split = qsplit(msg)
396 local plain = table.concat(popfirst(split), " ")
398 -- initial key trade
399 if split[1] == prefix .. "I" then
400 establish_receive(player, plain, true)
401 return true
402 -- key trade response
403 elseif split[1] == prefix .. "R" then
404 establish_receive(player, plain)
405 return true
406 -- encrypted message receive
407 elseif split[1] == prefix .. "E" then -- encrypt
408 message_receive(player, plain)
409 return true
413 -- remove friends if they leave
414 local player = player_left(message)
415 if player then
416 friends[player] = nil
422 minetest.register_globalstep(
423 function()
424 if os.time() > player_check_epoch + 2 then
425 wisp.players = minetest.get_player_names()
428 local p = peek()
429 if p then
430 if not in_list(wisp.players, peek().player) then
431 minetest.display_chat_message("Player " .. p.player .. " is not online. If they are please resend the message.")
432 dequeue()
433 return
436 if os.time() > p.time + timeout then
437 minetest.display_chat_message("Player " .. p.player .. " is not responsive.")
438 dequeue()
439 return
442 if (p.player == minetest.localplayer:get_name() and not p.force_send) or friends[p.player] then
443 local v = dequeue()
444 message_send(v.player, v.message, v.hide_to, v.force_send)
451 minetest.register_chatcommand("e", {
452 params = "<player>",
453 description = "Send encrypted whisper to player",
454 func = function(param)
455 local player = qsplit(param)[1]
456 local message = table.concat(popfirst(qsplit(param)), " ")
457 if player == nil then
458 minetest.display_chat_message("Player not specified.")
459 return
461 wisp.send(player, message)