From e4b1c935120580b013a6f9ba2212a4cb2ef246ac Mon Sep 17 00:00:00 2001
From: Auke Kok <sofar@foo-projects.org>
Date: Sat, 26 Dec 2015 11:16:49 -0800
Subject: [PATCH] Keys: Allow easy sharing of access without commands

This code adds the key concept to minetest_game, and integrates it
with lockable nodes. Currently supported lockable items are the Steel
Door, the Steel Trapdoor, and the Locked Chest.

The goal of this modification is to introduce a fine-grained multi-
player permission system that is intuitive and usable without any
console or chat commands, and doesn't require extra privileges to
be granted or setup. Keys can also physically be conveyed to other
players, adding to gameplay and adding some personality that is
preferable to console commands or editing formspecs.

A skeleton key can be crafted with 1 gold ingot. Skeleton keys can
then be matched to a lockable node by right-clicking the skeleton
key on a lockable node, which changes the skeleton key to a "key".

Gold was chosen as it's currently a not-so very useful item, and
therefore it's likely that players have some, but aren't really
using it for any purpose.

This key can subsequently used by any player to open or access that
lockable node, including retrieving items from Locked Chests, or
putting items in them.

They key is programmed to fit only the particular locked node it is
programmed to. This is achieved by storing a secret value in both
key and locked node. If this secret value doesn't match, the key
will not open the locked node. This allows many keys to be created
for one chest or door, but a key will only fit one node ever. The
secrets are stored in node, and item meta for the key.

If a locked node is removed, all keys that opened it are no longer
valid. Even if a new door/chest is placed in exactly the same spot,
the old keys will no longer fit that node.

Keys can be smelted back in gold ingots if they are no longer useful.

The method of storing a secret in nodemeta and itemstackmeta is secure
as there is no way for the client to create new items on the server
with a particular secret metadata value. Even if you could possible
create such an itemstack on the client, the server does not ever read
itemstackmeta from a client package.

The patch adds an API that allows other nodes and nodes added by
mods to use the same keys as well. The method how to implement this
is described in game_api.txt. The mod should add 2 callbacks to it's
node definition. Example code is given.

Textures are from PixelBOX, thanks to Gambit.
---
 game_api.txt                                  |  58 ++++++++++++++
 mods/default/README.txt                       |   2 +
 mods/default/crafting.lua                     |  21 +++++
 mods/default/nodes.lua                        |  57 +++++++++++++-
 mods/default/textures/default_key.png         | Bin 0 -> 3037 bytes
 .../default/textures/default_key_skeleton.png | Bin 0 -> 3043 bytes
 mods/default/tools.lua                        |  72 ++++++++++++++++++
 mods/doors/init.lua                           |  70 ++++++++++++++++-
 8 files changed, 274 insertions(+), 6 deletions(-)
 create mode 100644 mods/default/textures/default_key.png
 create mode 100644 mods/default/textures/default_key_skeleton.png

diff --git a/game_api.txt b/game_api.txt
index e85898f..db5fc01 100644
--- a/game_api.txt
+++ b/game_api.txt
@@ -672,3 +672,61 @@ Carts
 	like speed, acceleration, player attachment. The handler will
 	likely be called many times per second, so the function needs
 	to make sure that the event is handled properly.
+
+Key API
+-------
+
+The key API allows mods to add key functionality to nodes that have
+ownership or specific permissions. Using the API will make it so
+that a node owner can use skeleton keys on their nodes to create keys
+for that node in that location, and give that key to other players,
+allowing them some sort of access that they otherwise would not have
+due to node protection.
+
+To make your new nodes work with the key API, you need to register
+two callback functions in each nodedef:
+
+
+`on_key_use(pos, player)`
+ * Is called when a player right-clicks (uses) a normal key on your
+ * node.
+ * `pos` - position of the node
+ * `player` - PlayerRef
+ * return value: none, ignored
+
+The `on_key_use` callback should validate that the player is wielding
+a key item with the right key meta secret. If needed the code should
+deny access to the node functionality.
+
+If formspecs are used, the formspec callbacks should duplicate these
+checks in the metadata callback functions.
+
+
+`on_skeleton_key_use(pos, player, newsecret)`
+
+ * Is called when a player right-clicks (uses) a skeleton key on your
+ * node.
+ * `pos` - position of the node
+ * `player` - PlayerRef
+ * `newsecret` - a secret value(string)
+ * return values:
+ * `secret` - `nil` or the secret value that unlocks the door
+ * `name` - a string description of the node ("a locked chest")
+ * `owner` - name of the node owner
+
+The `on_skeleton_key_use` function should validate that the player has
+the right permissions to make a new key for the item. The newsecret
+value is useful if the node has no secret value. The function should
+store this secret value somewhere so that in the future it may compare
+key secrets and match them to allow access. If a node already has a
+secret value, the function should return that secret value instead
+of the newsecret value. The secret value stored for the node should
+not be overwritten, as this would invalidate existing keys.
+
+Aside from the secret value, the function should retun a descriptive
+name for the node and the owner name. The return values are all
+encoded in the key that will be given to the player in replacement
+for the wielded skeleton key.
+
+if `nil` is returned, it is assumed that the wielder did not have
+permissions to create a key for this node, and no key is created.
diff --git a/mods/default/README.txt b/mods/default/README.txt
index c76cf7c..9dde0eb 100644
--- a/mods/default/README.txt
+++ b/mods/default/README.txt
@@ -177,6 +177,8 @@ Gambit (CC BY-SA 3.0):
   default_snow.png
   default_snow_side.png
   default_snowball.png
+  default_key.png
+  default_key_skeleton.png
 
 asl97 (CC BY-SA 3.0):
   default_ice.png
diff --git a/mods/default/crafting.lua b/mods/default/crafting.lua
index 23f233f..50b4b95 100644
--- a/mods/default/crafting.lua
+++ b/mods/default/crafting.lua
@@ -352,6 +352,13 @@ minetest.register_craft({
 	}
 })
 
+minetest.register_craft({
+	output = 'default:skeleton_key',
+	recipe = {
+		{'default:gold_ingot'},
+	}
+})
+
 minetest.register_craft({
 	output = 'default:chest',
 	recipe = {
@@ -781,6 +788,20 @@ minetest.register_craft({
 	recipe = "default:clay_lump",
 })
 
+minetest.register_craft({
+	type = 'cooking',
+	output = 'default:gold_ingot',
+	recipe = 'default:skeleton_key',
+	cooktime = 5,
+})
+
+minetest.register_craft({
+	type = 'cooking',
+	output = 'default:gold_ingot',
+	recipe = 'default:key',
+	cooktime = 5,
+})
+
 --
 -- Fuels
 --
diff --git a/mods/default/nodes.lua b/mods/default/nodes.lua
index 9aa7af5..6e391e6 100644
--- a/mods/default/nodes.lua
+++ b/mods/default/nodes.lua
@@ -1619,16 +1619,30 @@ local function get_locked_chest_formspec(pos)
 end
 
 local function has_locked_chest_privilege(meta, player)
-	local name = ""
 	if player then
 		if minetest.check_player_privs(player, "protection_bypass") then
 			return true
 		end
-		name = player:get_player_name()
-	end
-	if name ~= meta:get_string("owner") then
+	else
 		return false
 	end
+
+	-- is player wielding the right key?
+	local item = player:get_wielded_item()
+	if item:get_name() == "default:key" then
+		local key_meta = minetest.parse_json(item.get_metadata())
+		local secret = meta:get_string("key_lock_secret")
+		if secret ~= key_meta.secret then
+			return false
+		end
+
+		return true
+	end
+
+	if player:get_player_name() ~= meta:get_string("owner") then
+		return false
+	end
+
 	return true
 end
 
@@ -1748,6 +1762,41 @@ minetest.register_node("default:chest_locked", {
 		return itemstack
 	end,
 	on_blast = function() end,
+	on_key_use = function(pos, player)
+		local secret = minetest.get_meta(pos):get_string("key_lock_secret")
+		local itemstack = player:get_wielded_item()
+		local key_meta = minetest.parse_json(itemstack:get_metadata())
+
+		if secret ~= key_meta.secret then
+			return
+		end
+
+		minetest.show_formspec(
+			player:get_player_name(),
+			"default:chest_locked",
+			get_locked_chest_formspec(pos)
+		)
+	end,
+	on_skeleton_key_use = function(pos, player, newsecret)
+		local meta = minetest.get_meta(pos)
+		local owner = meta:get_string("owner")
+		local name = player:get_player_name()
+
+		-- verify placer is owner of lockable chest
+		if owner ~= name then
+			minetest.record_protection_violation(pos, name)
+			minetest.chat_send_player(name, "You do not own this chest.")
+			return nil
+		end
+
+		local secret = meta:get_string("key_lock_secret")
+		if secret == "" then
+			secret = newsecret
+			meta:set_string("key_lock_secret", secret)
+		end
+
+		return secret, "a locked chest", owner
+	end,
 })
 
 
diff --git a/mods/default/textures/default_key.png b/mods/default/textures/default_key.png
new file mode 100644
index 0000000000000000000000000000000000000000..d59bfb6b2e74cb7b3b54eb9b2321f69cdfa13956
GIT binary patch
literal 3037
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

literal 0
HcmV?d00001

diff --git a/mods/default/textures/default_key_skeleton.png b/mods/default/textures/default_key_skeleton.png
new file mode 100644
index 0000000000000000000000000000000000000000..eafcc19585b3239ca7061a7f939e6507422dd2e9
GIT binary patch
literal 3043
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

literal 0
HcmV?d00001

diff --git a/mods/default/tools.lua b/mods/default/tools.lua
index 5a39615..9147f9b 100644
--- a/mods/default/tools.lua
+++ b/mods/default/tools.lua
@@ -378,3 +378,75 @@ minetest.register_tool("default:sword_diamond", {
 	},
 	sound = {breaks = "default_tool_breaks"},
 })
+
+minetest.register_tool("default:skeleton_key", {
+	description = "Skeleton Key",
+	inventory_image = "default_key_skeleton.png",
+	groups = {key = 1},
+	on_place = function(itemstack, placer, pointed_thing)
+		if pointed_thing.type ~= "node" then
+			return itemstack
+		end
+
+		local pos = pointed_thing.under
+		local node = minetest.get_node(pos)
+
+		if not node then
+			return itemstack
+		end
+
+		local on_skeleton_key_use = minetest.registered_nodes[node.name].on_skeleton_key_use
+		if on_skeleton_key_use then
+			-- make a new key secret in case the node callback needs it
+			local random = math.random
+			local newsecret = string.format(
+				"%04x%04x%04x%04x",
+				random(2^16) - 1, random(2^16) - 1,
+				random(2^16) - 1, random(2^16) - 1)
+
+			local secret, _, _ = on_skeleton_key_use(pos, placer, newsecret)
+
+			if secret then
+				-- finish and return the new key
+				itemstack:take_item()
+				itemstack:add_item("default:key")
+				itemstack:set_metadata(minetest.write_json({
+					secret = secret
+				}))
+				return itemstack
+			end
+		end
+		return nil
+	end
+})
+
+minetest.register_tool("default:key", {
+	description = "Key",
+	inventory_image = "default_key.png",
+	groups = {key = 1, not_in_creative_inventory = 1},
+	stack_max = 1,
+	on_place = function(itemstack, placer, pointed_thing)
+		if pointed_thing.type ~= "node" then
+			return itemstack
+		end
+
+		local pos = pointed_thing.under
+		local node = minetest.get_node(pos)
+
+		if not node or node.name == "ignore" then
+			return itemstack
+		end
+
+		local ndef = minetest.registered_nodes[node.name]
+		if not ndef then
+			return itemstack
+		end
+
+		local on_key_use = ndef.on_key_use
+		if on_key_use then
+			on_key_use(pos, placer)
+		end
+
+		return nil
+	end
+})
diff --git a/mods/doors/init.lua b/mods/doors/init.lua
index 364e7a8..c5d4a14 100644
--- a/mods/doors/init.lua
+++ b/mods/doors/init.lua
@@ -140,8 +140,17 @@ function _doors.door_toggle(pos, node, clicker)
 	end
 
 	if clicker and not minetest.check_player_privs(clicker, "protection_bypass") then
+		-- is player wielding the right key?
+		local item = clicker:get_wielded_item()
 		local owner = meta:get_string("doors_owner")
-		if owner ~= "" then
+		if item:get_name() == "default:key" then
+			local key_meta = minetest.parse_json(item:get_metadata())
+			local secret = meta:get_string("key_lock_secret")
+			if secret ~= key_meta.secret then
+				return false
+			end
+
+		elseif owner ~= "" then
 			if clicker:get_player_name() ~= owner then
 				return false
 			end
@@ -371,6 +380,30 @@ function doors.register(name, def)
 	if def.protected then
 		def.can_dig = can_dig_door
 		def.on_blast = function() end
+		def.on_key_use = function(pos, player)
+			local door = doors.get(pos)
+			door:toggle(player)
+		end
+		def.on_skeleton_key_use = function(pos, player, newsecret)
+			local meta = minetest.get_meta(pos)
+			local owner = meta:get_string("doors_owner")
+			local pname = player:get_player_name()
+
+			-- verify placer is owner of lockable door
+			if owner ~= pname then
+				minetest.record_protection_violation(pos, pname)
+				minetest.chat_send_player(pname, "You do not own this locked door.")
+				return nil
+			end
+
+			local secret = meta:get_string("key_lock_secret")
+			if secret == "" then
+				secret = newsecret
+				meta:set_string("key_lock_secret", secret)
+			end
+
+			return secret, "a locked door", owner
+		end
 	else
 		def.on_blast = function(pos, intensity)
 			minetest.remove_node(pos)
@@ -491,9 +524,18 @@ end
 function _doors.trapdoor_toggle(pos, node, clicker)
 	node = node or minetest.get_node(pos)
 	if clicker and not minetest.check_player_privs(clicker, "protection_bypass") then
+		-- is player wielding the right key?
+		local item = clicker:get_wielded_item()
 		local meta = minetest.get_meta(pos)
 		local owner = meta:get_string("doors_owner")
-		if owner ~= "" then
+		if item:get_name() == "default:key" then
+			local key_meta = minetest.parse_json(item:get_metadata())
+			local secret = meta:get_string("key_lock_secret")
+			if secret ~= key_meta.secret then
+				return false
+			end
+
+		elseif owner ~= "" then
 			if clicker:get_player_name() ~= owner then
 				return false
 			end
@@ -546,6 +588,30 @@ function doors.register_trapdoor(name, def)
 		end
 
 		def.on_blast = function() end
+		def.on_key_use = function(pos, player)
+			local door = doors.get(pos)
+			door:toggle(player)
+		end
+		def.on_skeleton_key_use = function(pos, player, newsecret)
+			local meta = minetest.get_meta(pos)
+			local owner = meta:get_string("doors_owner")
+			local pname = player:get_player_name()
+
+			-- verify placer is owner of lockable door
+			if owner ~= pname then
+				minetest.record_protection_violation(pos, pname)
+				minetest.chat_send_player(pname, "You do not own this trapdoor.")
+				return nil
+			end
+
+			local secret = meta:get_string("key_lock_secret")
+			if secret == "" then
+				secret = newsecret
+				meta:set_string("key_lock_secret", secret)
+			end
+
+			return secret, "a locked trapdoor", owner
+		end
 	else
 		def.on_blast = function(pos, intensity)
 			minetest.remove_node(pos)