diff --git a/game_api.txt b/game_api.txt
index e85898fd..db5fc014 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 c76cf7c3..9dde0eba 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 23f233fb..50b4b957 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 9aa7af59..6e391e63 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 00000000..d59bfb6b
Binary files /dev/null and b/mods/default/textures/default_key.png differ
diff --git a/mods/default/textures/default_key_skeleton.png b/mods/default/textures/default_key_skeleton.png
new file mode 100644
index 00000000..eafcc195
Binary files /dev/null and b/mods/default/textures/default_key_skeleton.png differ
diff --git a/mods/default/tools.lua b/mods/default/tools.lua
index 5a39615c..9147f9b3 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 364e7a8a..c5d4a140 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)