JackMacWindows

vericode.lua

Feb 20th, 2021 (edited)
316
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 12.63 KB | None | 0 0
  1. --- VeriCode - Easy code signing for ComputerCraft
  2. -- By JackMacWindows
  3. --
  4. -- @module vericode
  5. --
  6. -- Code signing uses encryption and hashes to easily verify a) that the sender of
  7. -- the code is trusted, and b) that the code hasn't been changed mid-transfer.
  8. -- VeriCode applies this concept to Lua code sent over Rednet to add a layer of
  9. -- security to Rednet. Just plainly receiving code from whoever sends it is
  10. -- dangerous, and invites the possibility of getting malware (in fact, I've made
  11. -- a virus that spreads through this method). Adding code signing ensures that
  12. -- any code received is safe and trusted.
  13. --
  14. -- Requires ecc library (pastebin get ZGJGBJdg ecc.lua)
  15.  
  16. --[[ Basic usage:
  17. 1. Generate keypair files with vericode.generateKeypair
  18. 2. Copy the .key.pub file (NOT the standard .key file!!!) to each client that
  19.    needs to receive signed code
  20. 3. Require the API & load the key (.key on server, .key.pub on clients) - on the
  21.    server, make sure to store the key returned from loadKey as you'll need it to send
  22. 4. Call vericode.send to send a Lua script to a client computer
  23. 5. Call vericode.receive on the client to listen for code from the server (note
  24.    that it returns after receiving a function, so call it in an infinite loop if
  25.    you want it to always accept code)
  26.  
  27. Example code:
  28.  
  29. -- On server:
  30. local vericode = require "vericode"
  31. if not fs.exists("mykey.key") then
  32.     vericode.generateKeypair("mykey.key")
  33.     print("Please copy mykey.key.pub to the client computer.")
  34.     return
  35. end
  36. local key = vericode.loadKey("mykey.key")
  37. vericode.send(otherComputerID, "turtle.forward()", key, "turtleInstructions")
  38.  
  39. -- On client:
  40. local vericode = require "vericode"
  41. vericode.loadKey("mykey.key.pub")
  42. while true do vericode.receive(true, "turtleInstructions") end
  43.  
  44. --]]
  45.  
  46. -- MIT License
  47. --
  48. -- Copyright (c) 2021 JackMacWindows
  49. --
  50. -- Permission is hereby granted, free of charge, to any person obtaining a copy
  51. -- of this software and associated documentation files (the "Software"), to deal
  52. -- in the Software without restriction, including without limitation the rights
  53. -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  54. -- copies of the Software, and to permit persons to whom the Software is
  55. -- furnished to do so, subject to the following conditions:
  56. --
  57. -- The above copyright notice and this permission notice shall be included in all
  58. -- copies or substantial portions of the Software.
  59. --
  60. -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  61. -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  62. -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  63. -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  64. -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  65. -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  66. -- SOFTWARE.
  67.  
  68. local function minver(version)
  69.     local res
  70.     if _CC_VERSION then res = version <= _CC_VERSION
  71.     elseif not _HOST then res = version <= os.version():gsub("CraftOS ", "")
  72.     else res = version <= _HOST:match("ComputerCraft ([0-9%.]+)") end
  73.     assert(res, "This program requires ComputerCraft " .. version .. " or later.")
  74. end
  75.  
  76. minver "1.91.0"
  77.  
  78. local expect = require "cc.expect".expect
  79. local ecc = require "ecc"
  80.  
  81. local vericode = {}
  82. local keyStore = {}
  83.  
  84. local b64str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
  85.  
  86. local function base64encode(str)
  87.     local retval = ""
  88.     for s in str:gmatch "..." do
  89.         local n = s:byte(1) * 65536 + s:byte(2) * 256 + s:byte(3)
  90.         local a, b, c, d = bit32.extract(n, 18, 6), bit32.extract(n, 12, 6), bit32.extract(n, 6, 6), bit32.extract(n, 0, 6)
  91.         retval = retval .. b64str:sub(a+1, a+1) .. b64str:sub(b+1, b+1) .. b64str:sub(c+1, c+1) .. b64str:sub(d+1, d+1)
  92.     end
  93.     if #str % 3 == 1 then
  94.         local n = str:byte(-1)
  95.         local a, b = bit32.rshift(n, 2), bit32.lshift(bit32.band(n, 3), 4)
  96.         retval = retval .. b64str:sub(a+1, a+1) .. b64str:sub(b+1, b+1) .. "=="
  97.     elseif #str % 3 == 2 then
  98.         local n = str:byte(-2) * 256 + str:byte(-1)
  99.         local a, b, c, d = bit32.extract(n, 10, 6), bit32.extract(n, 4, 6), bit32.lshift(bit32.extract(n, 0, 4), 2)
  100.         retval = retval .. b64str:sub(a+1, a+1) .. b64str:sub(b+1, b+1) .. b64str:sub(c+1, c+1) .. "="
  101.     end
  102.     return retval
  103. end
  104.  
  105. local function base64decode(str)
  106.     local retval = ""
  107.     for s in str:gmatch "...." do
  108.         if s:sub(3, 4) == '==' then
  109.             retval = retval .. string.char(bit32.bor(bit32.lshift(b64str:find(s:sub(1, 1)) - 1, 2), bit32.rshift(b64str:find(s:sub(2, 2)) - 1, 4)))
  110.         elseif s:sub(4, 4) == '=' then
  111.             local n = (b64str:find(s:sub(1, 1))-1) * 4096 + (b64str:find(s:sub(2, 2))-1) * 64 + (b64str:find(s:sub(3, 3))-1)
  112.             retval = retval .. string.char(bit32.extract(n, 10, 8)) .. string.char(bit32.extract(n, 2, 8))
  113.         else
  114.             local n = (b64str:find(s:sub(1, 1))-1) * 262144 + (b64str:find(s:sub(2, 2))-1) * 4096 + (b64str:find(s:sub(3, 3))-1) * 64 + (b64str:find(s:sub(4, 4))-1)
  115.             retval = retval .. string.char(bit32.extract(n, 16, 8)) .. string.char(bit32.extract(n, 8, 8)) .. string.char(bit32.extract(n, 0, 8))
  116.         end
  117.     end
  118.     return retval
  119. end
  120.  
  121. vericode.base64 = {encode = base64encode, decode = base64decode}
  122. vericode.sha256 = ecc.sha256
  123. vericode.random = ecc.random
  124. vericode.ecc = ecc
  125.  
  126. --- Generates a keypair for code signing.
  127. -- Outputs a pub/priv keypair at `path`, and a public-only key (for receivers) at `path`.pub.
  128. -- The generated key will be added to the store.
  129. -- @param path string The path to the file to generate.
  130. -- @return string pub The new public key.
  131. -- @return string priv The new private key. (Not required for this API, but might be useful otherwise.)
  132. function vericode.generateKeypair(path)
  133.     expect(1, path, "string")
  134.     local priv, pub = ecc.keypair(ecc.random.random())
  135.     pub, priv = base64encode(string.char(table.unpack(pub))), base64encode(string.char(table.unpack(priv)))
  136.     local file, err = fs.open(path, "w")
  137.     if not file then error("Could not open certificate file: " .. err, 2) end
  138.     file.write(textutils.serialize({
  139.         public = pub,
  140.         private = priv
  141.     }))
  142.     file.close()
  143.     file, err = fs.open(path .. ".pub", "w")
  144.     if not file then error("Could not open public certificate file: " .. err, 2) end
  145.     file.write(textutils.serialize({
  146.         public = pub
  147.     }))
  148.     file.close()
  149.     keyStore[pub] = {
  150.         public = pub,
  151.         private = priv
  152.     }
  153.     return pub, priv
  154. end
  155.  
  156. --- Loads a key from disk. This can be a full keypair, or only a public key.
  157. -- @param path string The path to the key.
  158. -- @return string key The loaded public key.
  159. function vericode.loadKey(path)
  160.     expect(1, path, "string")
  161.     local file, err = fs.open(path, "r")
  162.     if not file then error("Could not open certificate file: " .. err, 2) end
  163.     local t = textutils.unserialize(file.readAll())
  164.     file.close()
  165.     if type(t) ~= "table" or t.public == nil then error("Invalid certificate file", 2) end
  166.     keyStore[t.public] = t
  167.     return t.public
  168. end
  169.  
  170. --- Adds a public (and private if provided) key to the key store.
  171. -- @param pub string The public key to add.
  172. -- @param priv string|nil The private key to add, if desired.
  173. function vericode.addKey(pub, priv)
  174.     expect(1, pub, "string")
  175.     expect(2, priv, "string", "nil")
  176.     keyStore[pub] = {
  177.         public = pub,
  178.         private = priv
  179.     }
  180. end
  181.  
  182. --- Compiles, dumps, and signs a Lua chunk.
  183. -- @param code string The Lua code to compile.
  184. -- @param key string The public or private key to use. The private key associated with this key must exist in the key store.
  185. -- @return string chunk A signed and compiled Lua chunk. This chunk can either be loaded with `load` here, or standard Lua `load`.
  186. function vericode.dump(code, key)
  187.     expect(1, code, "string")
  188.     expect(2, key, "string")
  189.     local pub, priv
  190.     if keyStore[key] then
  191.         if not keyStore[key].private then error("No private key associated with selected public key", 2) end
  192.         pub, priv = key, keyStore[key].private
  193.     else
  194.         for _,v in pairs(keyStore) do
  195.             if v.public == key then
  196.                 if not v.private then error("No private key associated with selected public key", 2) end
  197.                 pub, priv = key, v.private
  198.                 break
  199.             elseif v.private == key then
  200.                 pub, priv = v.public, key
  201.                 break
  202.             end
  203.         end
  204.     end
  205.     if not pub or not priv then error("Could not find private key", 2) end
  206.     local fn, err = load(code, "=temp")
  207.     if not fn then error("Could not load chunk: " .. err, 2) end
  208.     local dump = string.dump(fn)
  209.     local size_t = dump:byte(9)
  210.     local chunk = dump:sub(19 + size_t)
  211.     local name = "=signed-chunk:" .. pub .. ":" .. base64encode(string.char(table.unpack(ecc.sign(base64decode(priv), chunk)))) .. "\0"
  212.     return dump:sub(1, 12) .. string.pack("I" .. size_t, #name) .. name .. chunk
  213. end
  214.  
  215. --- Loads and verifies a previously signed code chunk.
  216. -- The public key associated with the chunk must be present in the key store.
  217. -- @param code string The code chunk to load.
  218. -- @param name string|nil The name of the chunk.
  219. -- @param _mode nil Ignored (for compatibility).
  220. -- @param env table|nil The environment to give the chunk.
  221. -- @return function|nil fn The returned function, or nil on error.
  222. -- @return nil|string err If an error occurred, the error message.
  223. function vericode.load(code, name, _mode, env)
  224.     expect(1, code, "string")
  225.     expect(2, name, "string", "nil")
  226.     expect(4, env, "table", "nil")
  227.     if code:sub(1, 5) ~= "\x1bLuaQ" then return nil, "Not a compiled Lua chunk" end
  228.     local size_t = code:byte(9)
  229.     local codename = code:sub(13 + size_t, 12 + size_t + string.unpack("I" .. size_t, code:sub(13, 12 + size_t)))
  230.     local chunk = code:sub(13 + size_t + #codename)
  231.     local key, sig = codename:match "^=signed%-chunk:([A-Za-z0-9+/]+=*):([A-Za-z0-9+/]+=*)\0$"
  232.     if not key or not sig then return nil, "Not signed" end
  233.     if not keyStore[key] then return nil, "Unrecognized key: " .. key end
  234.     if not ecc.verify(base64decode(key), chunk, {base64decode(sig):byte(1, -1)}) then return nil, "Invalid code signature" end
  235.     if name then code = code:sub(1, 12) .. string.pack("I" .. size_t, #name + 1) .. name .. "\0" .. chunk end
  236.     return load(code, name, "b", env)
  237. end
  238.  
  239. --- Sends a signed code chunk over Rednet.
  240. -- @param recipient number The ID of the recipient.
  241. -- @param code string The code chunk to send.
  242. -- @param key string The key to use to sign the chunk.
  243. -- @param protocol string|nil The protocol to set, if desired.
  244. -- @return boolean ok Whether the message was sent.
  245. function vericode.send(recipient, code, key, protocol)
  246.     expect(1, recipient, "number")
  247.     expect(2, code, "string")
  248.     expect(3, key, "string")
  249.     expect(4, protocol, "string", "nil")
  250.     return rednet.send(recipient, vericode.dump(code, key), protocol)
  251. end
  252.  
  253. --- Waits to receive a signed code chunk, and either returns the loaded function or the results from calling it.
  254. -- @param run boolean|nil Whether to run the code, or just return the function.
  255. -- @param filter string|nil The name of the protocol to listen for (nil for any).
  256. -- @param timeout number|nil The maximum amount of time to wait.
  257. -- @param name string|nil The name to give the loaded chunk (defaults to "=VeriCode chunk").
  258. -- @param env table|nil The environment to give the function.
  259. -- @return any res Either the loaded function, or the results from the function, or nil if the timeout was passed.
  260. function vericode.receive(run, filter, timeout, name, env)
  261.     expect(1, run, "boolean", "nil")
  262.     expect(2, filter, "string", "nil")
  263.     expect(3, timeout, "number", "nil")
  264.     expect(4, name, "string", "nil")
  265.     expect(5, env, "table", "nil")
  266.     local res = {n = 0}
  267.     local function receive()
  268.         while true do
  269.             local _, message = rednet.receive(filter)
  270.             if type(message) == "string" then
  271.                 local fn = vericode.load(message, name or "=VeriCode chunk", nil, env)
  272.                 if fn then
  273.                     if run then res = table.pack(fn())
  274.                     else res = {fn, n = 1} end
  275.                     return table.unpack(res, 1, res.n)
  276.                 end
  277.             end
  278.         end
  279.     end
  280.     if timeout then
  281.         parallel.waitForAny(receive, function() sleep(timeout) end)
  282.         return table.unpack(res, 1, res.n)
  283.     else return receive() end
  284. end
  285.  
  286. if ... then vericode.generateKeypair(...) end
  287.  
  288. return vericode
  289.  
Add Comment
Please, Sign In to add comment