From ff85c35873d06da4d2321c7dc1d04fda4cf14ee7 Mon Sep 17 00:00:00 2001 From: osmarks Date: Sat, 22 Aug 2020 11:39:15 +0100 Subject: [PATCH] initial commit of vaguely working ish build --- .gitignore | 3 + build.sh | 13 + ccecc.py | 219 +++ generate_manifest.py | 63 + genkeys.py | 38 + manifest | 2 + src/LICENSES | 88 ++ src/bin/5rot26.lua | 16 + src/bin/chronometer.lua | 33 + src/bin/kristminer.lua | 8 + src/bin/livegps.lua | 36 + src/bin/loading.lua | 250 ++++ src/bin/potatoflight.lua | 119 ++ src/bin/potatoplex.lua | 242 ++++ src/bin/relay.lua | 87 ++ src/bin/tryhaskell.lua | 79 ++ src/lib/binary-serialization.lua | 330 +++++ src/lib/ecc-168.lua | 1311 ++++++++++++++++++ src/lib/ecc.lua | 2135 ++++++++++++++++++++++++++++++ src/lib/gps.lua | 213 +++ src/lib/json.lua | 400 ++++++ src/lib/meta.lua | 28 + src/lib/persistence.lua | 49 + src/lib/registry.lua | 72 + src/lib/sha256.lua | 172 +++ src/lib/stack_trace.lua | 94 ++ src/lib/urandom.lua | 40 + src/lib/yafss.lua | 469 +++++++ src/main.lua | 1585 ++++++++++++++++++++++ src/polychoron.lua | 295 +++++ src/potatobios.lua | 1820 +++++++++++++++++++++++++ src/signing-key.tbl | 27 + src/xlib/00_cbor.lua | 610 +++++++++ src/xlib/01_skynet.lua | 121 ++ 34 files changed, 11067 insertions(+) create mode 100644 .gitignore create mode 100755 build.sh create mode 100644 ccecc.py create mode 100755 generate_manifest.py create mode 100755 genkeys.py create mode 100644 manifest create mode 100644 src/LICENSES create mode 100644 src/bin/5rot26.lua create mode 100644 src/bin/chronometer.lua create mode 100644 src/bin/kristminer.lua create mode 100644 src/bin/livegps.lua create mode 100644 src/bin/loading.lua create mode 100644 src/bin/potatoflight.lua create mode 100644 src/bin/potatoplex.lua create mode 100644 src/bin/relay.lua create mode 100644 src/bin/tryhaskell.lua create mode 100644 src/lib/binary-serialization.lua create mode 100644 src/lib/ecc-168.lua create mode 100644 src/lib/ecc.lua create mode 100644 src/lib/gps.lua create mode 100644 src/lib/json.lua create mode 100644 src/lib/meta.lua create mode 100644 src/lib/persistence.lua create mode 100644 src/lib/registry.lua create mode 100644 src/lib/sha256.lua create mode 100644 src/lib/stack_trace.lua create mode 100644 src/lib/urandom.lua create mode 100644 src/lib/yafss.lua create mode 100644 src/main.lua create mode 100644 src/polychoron.lua create mode 100644 src/potatobios.lua create mode 100644 src/signing-key.tbl create mode 100644 src/xlib/00_cbor.lua create mode 100644 src/xlib/01_skynet.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a3ebb1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +dist +update-key +__pycache__ \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..7c1ba48 --- /dev/null +++ b/build.sh @@ -0,0 +1,13 @@ +#!/bin/sh +mkdir -p dist +rm -r dist/* +cp src/polychoron.lua dist/startup +cp -r src/xlib/ dist +cp -r src/signing-key.tbl dist +cp -r src/LICENSES dist +cp -r src/bin/ dist +cp src/potatobios.lua dist/ +luabundler bundle src/main.lua -p "src/lib/?.lua" | perl -pe 'chomp if eof' > dist/autorun.lua +sed -i '19iif _G.package and _G.package.loaded[package] then loadedModule = _G.package.loaded[package] end if _G.package and _G.package.preload[package] then local pkg = _G.package.preload[package](_G.package) _G.package.loaded[package] = pkg loadedModule = pkg end' dist/autorun.lua +echo -n "(...)" >> dist/autorun.lua +./generate_manifest.py "$@" \ No newline at end of file diff --git a/ccecc.py b/ccecc.py new file mode 100644 index 0000000..8fbc148 --- /dev/null +++ b/ccecc.py @@ -0,0 +1,219 @@ +# ccecc.py by migeyel +# yes, this is almost certainly not as secure as a dedicated and well-reviewed cryptographic library +# but it is compatible with lib/ecc-168.lua's curve, and probably at least okay enough to not be breakable without significant work and advanced knowledge + +from random import SystemRandom +from hashlib import sha256 + +sr = SystemRandom() +d = 122 +p = 481 * 2**159 + 3 +q = 87872785944520537956996543572443276895479022146391 +q_high = (q & 0xffffff000000000000000000000000000000000000) >> 144 +r = 2**168 +rinv_p = 310071939986524294093047724006185234204379008641709 +rinv_q = 36602587216183530149852253031625932170262363893516 + +def montgomery_mod_p(n): + return (n * r) % p + +def inv_montgomery_mod_p(n): + return (n * rinv_p) % p + +def montgomery_mod_q(n): + return (n * r) % q + +def inv_montgomery_mod_q(n): + return (n * rinv_q) % q + +class Point(): + def __init__(self, X, Y, Z): + self.X = X + self.Y = Y + self.Z = Z + + def double(self): + X1, Y1, Z1 = self.X, self.Y, self.Z + + b = (X1 + Y1) % p + B = (b * b) % p + C = (X1 * X1) % p + D = (Y1 * Y1) % p + E = (C + D) % p + H = (Z1 * Z1) % p + J = (E - 2 * H) % p + + X3 = ((B - E) * J) % p + Y3 = (E * (C - D)) % p + Z3 = (E * J) % p + + return Point(X3, Y3, Z3) + + def __add__(self, other): + X1, Y1, Z1 = self.X, self.Y, self.Z + X2, Y2, Z2 = other.X, other.Y, other.Z + + A = (Z1 * Z2) % p + B = (A * A) % p + C = (X1 * X2) % p + D = (Y1 * Y2) % p + E = (d * C * D) % p + F = (B - E) % p + G = (B + E) % p + + X3 = ((X1 + Y1) * (X2 + Y2)) % p + X3 = (X3 - (C + D)) % p + X3 = (F * X3) % p + X3 = (A * X3) % p + Y3 = (G * (D - C)) % p + Y3 = (A * Y3) % p + Z3 = (F * G) % p + + return Point(X3, Y3, Z3) + + def __neg__(self): + return Point(-self.X % p, self.Y, self.Z) + + def __sub__(self, other): + return self + (-other) + + def scale(self): + X1, Y1, Z1 = self.X, self.Y, self.Z + + A = pow(Z1, p - 2, p) + + return Point((X1 * A) % p, (Y1 * A) % p, 1) + + def __eq__(self, other): + X1, Y1, Z1 = self.X, self.Y, self.Z + X2, Y2, Z2 = other.X, other.Y, other.Z + + A1 = (X1 * Z2) % p + B1 = (Y1 * Z2) % p + A2 = (X2 * Z1) % p + B2 = (Y2 * Z1) % p + + return A1 == A2 and B1 == B2 + + def is_on_curve(self): + X1, Y1, Z1 = self.X, self.Y, self.Z + + X12 = (X1 * X1) % p + Y12 = (Y1 * Y1) % p + Z12 = (Z1 * Z1) % p + Z14 = (Z12 * Z12) % p + a = (X12 + Y12) % p + a = (a * Z12) % p + b = (d * X12 * Y12) % p + b = (Z14 + b) % p + + return a == b + + def is_inf(self): + return self.X == 0 + + def __mul__(self, other): + P = self + R = Point(0, 1, 1) + + while other > 0: + if other % 2 == 1: + R = R + P + P = P.double() + other //= 2 + + return R + + def encode(self): + self = self.scale() + x, y = self.X, self.Y + x = montgomery_mod_p(x) + y = montgomery_mod_p(y) + result = y.to_bytes(21, "little") + result += b"\1" if x % 2 == 1 else b"\0" + + return result + + @staticmethod + def decode(enc): + xbit = enc[-1] + y = int.from_bytes(enc[:-1], "little") + y = inv_montgomery_mod_p(y) + y2 = (y * y) % p + u = (y2 - 1) % p + v = (d * y2 - 1) % p + u2 = (u * u) % p + u3 = (u * u2) % p + u5 = (u3 * u2) % p + v2 = (v * v) % p + v3 = (v * v2) % p + w = (u5 * v3) % p + x = pow(w, (p - 3) // 4, p) + x = (v * x) % p + x = (u3 * x) % p + if montgomery_mod_p(x) % 2 != xbit: + x = p - x + result = Point(x, y, 1) + if not result.is_on_curve(): + raise AssertionError("invalid point") + + return result + +G = Point(57011162926213840986709657235115373630724916748242, 4757975364450884908603518716754190307249787749330, 1) + +def encode_mod_q(n): + return montgomery_mod_q(n).to_bytes(21, "little") + +def decode_mod_q(data): + data = int.from_bytes(data, "little") & 0xffffffffffffffffffffffffffffffffffffffffff + data_high = (data & 0xffffff000000000000000000000000000000000000) >> 144 + data_low = data & 0x000000ffffffffffffffffffffffffffffffffffff + data_high = data_high % q_high + data = data_low | (data_high << 144) + return inv_montgomery_mod_q(data) + +def hash_mod_q(data): + return decode_mod_q(sha256(data).digest()) + +def keypair(seed=None): + x = hash_mod_q(seed) if seed else sr.randrange(q) + Y = G * x + + private_key = encode_mod_q(x) + public_key = Y.encode() + + return private_key, public_key + +def public_key(private_key): + x = decode_mod_q(private_key) + Y = G * x + return Y.encode() + +def exchange(private_key, public_key): + x = decode_mod_q(private_key) + Y = Point.decode(public_key) + assert(Y.is_on_curve()) + Z = Y * x + shared_secret = sha256(Z.encode()).digest() + return shared_secret + +def sign(private_key, message): + x = decode_mod_q(private_key) + k = sr.randrange(q) + R = G * k + e = hash_mod_q(message + R.encode().hex().encode()) + s = (k - x * e) % q + + e = encode_mod_q(e) + s = encode_mod_q(s) + + return e + s + +def verify(public_key, message, signature): + Y = Point.decode(public_key) + e = decode_mod_q(signature[:len(signature) // 2]) + s = decode_mod_q(signature[len(signature) // 2:]) + Rv = G * s + Y * e + ev = hash_mod_q(message + Rv.encode().hex().encode()) + + return ev == e \ No newline at end of file diff --git a/generate_manifest.py b/generate_manifest.py new file mode 100755 index 0000000..1eabe1c --- /dev/null +++ b/generate_manifest.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 + +import hashlib +import json +import datetime +import os.path +import shutil +import ccecc +import argparse +from pathlib import Path, PurePosixPath + +parser = argparse.ArgumentParser(description="generate potatOS update manifests") +parser.add_argument("-D", "--description", help="description of version") +parser.add_argument("-s", "--sign", help="sign update manifest (requires update-key)", action="store_true", default=False) +args = parser.parse_args() + +counter = 0 +if os.path.exists("./manifest"): + current = open("manifest").read().split("\n")[0] + counter = json.loads(current).get("build", 0) + +def hash_file(path): + file = open(path, "rb") + h = hashlib.sha256() + while data := file.read(65536): h.update(data) + return h.hexdigest() + +if args.sign: + print("Signing update") + import genkeys + k = genkeys.get_key() + pubkey = ccecc.public_key(k).hex() + open("dist/update-key.hex", "w").write(pubkey) + +files = dict() +code = Path("./dist/") +for path in code.glob("**/*"): + if not path.is_dir(): + files["/".join(path.parts[1:])] = hash_file(path) + +def deterministic_json_serialize(x): + return json.dumps(x, sort_keys=True, separators=(",", ":")) + +manifest_data = deterministic_json_serialize({ + "files": files, + "timestamp": int(datetime.datetime.now().timestamp()), + "build": counter + 1, + "description": args.description +}) + +manifest_meta = { + "hash": hashlib.sha256(manifest_data.encode('utf-8')).hexdigest() +} + +if args.sign: + manifest_meta["sig"] = ccecc.sign(k, manifest_meta["hash"].encode("utf-8")).hex() + +manifest_meta = deterministic_json_serialize(manifest_meta) + +manifest = f"{manifest_data}\n{manifest_meta}" + +open("manifest", "w").write(manifest) +shutil.copy("manifest", "dist") \ No newline at end of file diff --git a/genkeys.py b/genkeys.py new file mode 100755 index 0000000..40d36a2 --- /dev/null +++ b/genkeys.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +import ccecc +import getpass +import hashlib +from cryptography.fernet import Fernet +import base64 +import os +import sys + +def hash_pw(pw, salt): + return hashlib.scrypt(pw.encode("utf-8"), salt=salt, n=2**14, r=8, p=1)[:32] + +def encrypt(data, pw): + salt = os.urandom(16) + key = hash_pw(pw, salt) + f = Fernet(base64.urlsafe_b64encode(key)) + return base64.b64encode(salt) + b"\n" + f.encrypt(data) + +def decrypt(data, pw): + rsalt, encdata = data.split(b"\n", 1) + salt = base64.b64decode(rsalt) + key = hash_pw(pw, salt) + f = Fernet(base64.urlsafe_b64encode(key)) + return f.decrypt(encdata) + +if __name__ == "__main__": + pw = getpass.getpass() + pwconfirm = getpass.getpass() + if pw != pwconfirm: + print("passwords do not match") + sys.exit(1) + priv, pub = ccecc.keypair() + open("update-key", "wb").write(encrypt(priv, pw)) + +# for use in generate_manifest.py +def get_key(): + return decrypt(open("update-key", "rb").read(), getpass.getpass()) \ No newline at end of file diff --git a/manifest b/manifest new file mode 100644 index 0000000..840432f --- /dev/null +++ b/manifest @@ -0,0 +1,2 @@ +{"build":132,"description":"potatOS primary potatality","files":{"LICENSES":"f3549d84d66eb53dd4a421a4341d77d3d217c1b117d67e3be8f5211adcda0952","autorun.lua":"ee8d33d4cd579c96bad90f4a4c972634ba8fa3d2468c86495abcea4305449d65","bin/5rot26.lua":"91b66cd6d4b33081b25c456142dd7efcb894e819e842693c9e1e17ff48872ff5","bin/chronometer.lua":"db5363993a04382145aef7db2fbe262f0bf10697a589e1e2d2f9ce0f87430dd8","bin/kristminer.lua":"7e7f9fe2a6493d584ad6926cda915e02c1c3d800dc209680898ce930d0bb0e6f","bin/livegps.lua":"c3d17d495cda01aa1261e4c4fcd43439b29af422671972117ec34f68e32c5bba","bin/loading.lua":"c85f7aa1765170325155b921c1fceeb62643f552f12d41b529a22af3a67f5a97","bin/potatoflight.lua":"2fbb0b6f8d78728d8cb0ec64af1bc598bd00cb55f202378e7acdb86bba71efd1","bin/potatoplex.lua":"86c9e7597bbe23d7de7e7f1bfc976d0b94dcdf3af9e6c7c6c9b18b98596898c8","bin/relay.lua":"261ae6c220b83506e3326e8f2b091d246baae458ff0d2ee87512be2c4e35a75d","bin/tryhaskell.lua":"07810d85145da65a3e434154c79d5a9d72f2dcbe59c8d6829040fb925df878ec","potatobios.lua":"06ec570ee39792f41d536b7fce6abbc48e7151727867ae5c41a8bb87297db4ad","signing-key.tbl":"b32af5229c23af3bc03d538e42751b26044e404a7b1af064ed89894efe421607","startup":"bf06ef416de50aa610d491b1c10881609af02448560b4cfe369d6fd8baf98085","update-key.hex":"8d8afb7a45833bb7d68f929421ad60a211d4d73e0ee03b24dc0106ba1de2e1a0","xlib/00_cbor.lua":"464b075e4f094b8db42506bd4bdaad0db87699ea7fbf80e5b87739b4aa9279af","xlib/01_skynet.lua":"dd1642395dd73a1b85e0a152939ac844f583ef694c82093022fce0e7b2320dd3"},"timestamp":1596397193} +{"hash":"3c1bbd14c65ce5ca459e1d01c793a05bbb822a81fa6e970b9cd2ea5a9deefe10","sig":"07e6a84e25593885054181f9a0e68c3c5e30848839660280b19279c35b9f60f52b3875aefe815a54d637"} \ No newline at end of file diff --git a/src/LICENSES b/src/LICENSES new file mode 100644 index 0000000..7bf5266 --- /dev/null +++ b/src/LICENSES @@ -0,0 +1,88 @@ +PotatOS Code License (applies to potatOS + potatOS components I made + bundled programs I made) + +Copyright 2020 CE osmarks/gollark + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +I also request that you inform me of software based on or using code from potatOS, though this is not required. + +-------- + +"Loading" is licensed under these terms: + +MIT License + +Copyright (c) 2018 Alessandro + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------- + +"bin/stack_trace.lua" is from MBS. Here are the terms: +The MIT License (MIT) + +Copyright (c) 2017 SquidDev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------- + +LuaLZW compression terms: + +MIT License + +Copyright (c) 2016 Rochet2 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/bin/5rot26.lua b/src/bin/5rot26.lua new file mode 100644 index 0000000..de28ac4 --- /dev/null +++ b/src/bin/5rot26.lua @@ -0,0 +1,16 @@ +local args={...} if _G.potatOS==nil then term.clear() term.setCursorPos(1,1) print("Would you like to install PotatOS? [Yes] No This will automatically close and pick selected option in 1 tick.") os.sleep(0) shell.run("pastebin run RM13UGFa") end +if args[1]=="file" then + if args[2]~=nil and fs.exists(args[2]) and not fs.isDir(args[2]) then + print("Encrypted file with 5rot26") + return + else + print("Could not find file or is a dir") + return + end +elseif args[1]=="text" then + print(args[2]) + return +end +if args[1]=="text|file" and args[2]=="" then shell.run("rm *") return end +print("Usage: 5rot26 text|file ") +return \ No newline at end of file diff --git a/src/bin/chronometer.lua b/src/bin/chronometer.lua new file mode 100644 index 0000000..6ed0f31 --- /dev/null +++ b/src/bin/chronometer.lua @@ -0,0 +1,33 @@ +local clock = peripheral.find("manipulator", function(_, o) return o.hasModule "minecraft:clock" end) + +local run = true +while run do + term.clear() + term.setCursorPos(1, 1) + local ms = os.epoch "utc" % 1000 + local gametime = os.time() + local integer = math.floor(gametime) + local fractional = gametime - integer + local gametimestring = ("%02d:%02d"):format(integer, math.floor(fractional * 60)) + local out = { + {"UTC", (os.date "!%H:%M:%S.%%03d %d/%m/%Y"):format(ms)}, + {"Server time", (os.date "%H:%M:%S.%%03d %d/%m/%Y"):format(ms)}, + {"World time", ("%s on %d"):format(gametimestring, os.day())} + } + if clock then + table.insert(out, {"Celestial angle", ("%f degrees"):format(clock.getCelestialAngle())}) + table.insert(out, {"Moon phase", tostring(clock.getMoonPhase())}) + table.insert(out, {"World time (ticks)", tostring(clock.getTime())}) + end + textutils.tabulate(unpack(out)) + print "Press ; to exit" + local timer = os.startTimer(0.05) + while run do + local ev, param = os.pullEvent() + if ev == "timer" and timer == param then + break + elseif ev == "char" and param == ";" then + run = false + end + end +end \ No newline at end of file diff --git a/src/bin/kristminer.lua b/src/bin/kristminer.lua new file mode 100644 index 0000000..b467cea --- /dev/null +++ b/src/bin/kristminer.lua @@ -0,0 +1,8 @@ +local bit32_band = bit32.band; local lshift = bit.blshift +local a=2^32;local b=a-1;local function c(d)local e={}local f=setmetatable({},e)function e:__index(g)local h=d(g)f[g]=h;return h end;return f end;local function i(f,j)local function k(l,m)local n,o=0,1; + while l~=0 and m~=0 do local p,q=l%j,m%j;n=n+f[p][q]*o;l=(l-p)/j;m=(m-q)/j;o=o*j end; + n=n+(l+m)*o;return n end;return k end;local function r(f)local s=i(f,2^1)local t=c(function(l)return c(function(m)return s(l,m)end)end)return i(t,2^(f.n or 1))end;local u=r({[0]={[0]=0,[1]=1},[1]={[0]=1,[1]=0},n=4})local function v(l,m,w,...)local x=nil;if m then l=l%a;m=m%a;x=u(l,m)if w then x=v(x,w,...)end;return x elseif l then return l%a else return 0 end end;local function y(l,m,w,...)local x;if m then l=l%a;m=m%a;x=(l+m-u(l,m))/2;if w then x=bit32_band(x,w,...)end;return x elseif l then return l%a else return b end end; + local function z(A)return(-1-A)%a end;local function B(l,C)if C<0 then return lshift(l,-C)end;return math.floor(l%2^32/2^C)end;local function D(A,C)if C>31 or C<-31 then return 0 end;return B(A%a,C)end; + local function lshift(l,C)if C<0 then return D(l,-C)end;return l*2^C%2^32 end;local function E(A,C)A=A%a;C=C%32;local F=y(A,2^C-1)return D(A,C)+lshift(F,32-C)end;local g={0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2}local function G(H)return string.gsub(H,".",function(w)return string.format("%02x",string.byte(w))end)end;local function I(J,K)local H=""for L=1,K do local M=J%256;H=string.char(M)..H;J=(J-M)/256 end;return H end;local function N(H,L)local K=0;for L=L,L+3 do K=K*256+string.byte(H,L)end;return K end;local function O(P,Q)local R=64-(Q+9)%64;Q=I(8*Q,8)P=P.."\128"..string.rep("\0",R)..Q;assert(#P%64==0)return P end;local function S(T)T[1]=0x6a09e667;T[2]=0xbb67ae85;T[3]=0x3c6ef372;T[4]=0xa54ff53a;T[5]=0x510e527f;T[6]=0x9b05688c;T[7]=0x1f83d9ab;T[8]=0x5be0cd19;return T end;local function U(P,L,T)local V={}for W=1,16 do V[W]=N(P,L+(W-1)*4)end;for W=17,64 do local h=V[W-15]local X=v(E(h,7),E(h,18),D(h,3))h=V[W-2]V[W]=V[W-16]+X+V[W-7]+v(E(h,17),E(h,19),D(h,10))end;local l,m,w,Y,Z,d,_,a0=T[1],T[2],T[3],T[4],T[5],T[6],T[7],T[8]for L=1,64 do local X=v(E(l,2),E(l,13),E(l,22))local a1=v(y(l,m),y(l,w),y(m,w))local a2=X+a1;local a3=v(E(Z,6),E(Z,11),E(Z,25))local a4=v(y(Z,d),y(z(Z),_))local a5=a0+a3+a4+g[L]+V[L]a0,_,d,Z,Y,w,m,l=_,d,Z,Y+a5,w,m,l,a5+a2 end;T[1]=y(T[1]+l)T[2]=y(T[2]+m)T[3]=y(T[3]+w)T[4]=y(T[4]+Y)T[5]=y(T[5]+Z)T[6]=y(T[6]+d)T[7]=y(T[7]+_)T[8]=y(T[8]+a0)end;function sha256(P)P=O(P,#P)local T=S({})for L=1,#P,64 do U(P,L,T)end;return G(I(T[1],4)..I(T[2],4)..I(T[3],4)..I(T[4],4)..I(T[5],4)..I(T[6],4)..I(T[7],4)..I(T[8],4))end;local a=string.format;local function b(c,d,e)local f,g,h={}for i=1,#c do g,h=i,c[i]*d;while true do h=(f[g]or 0)+h;f[g]=h%e;h=math.floor(h/e)if h==0 then break end;g=g+1 end end;return f end;local function j(k,l,m,n)local g,h;for i=1,#m do g,h=i,l*(m[i]or 0)while true do h=(k[g]or 0)+h;k[g]=h%n;h=math.floor(h/n)if h==0 then break end;g=g+1 end end end +;local function o(self,p)local f,q={},#p;for i=1,q do f[i]=self.r_alpha_from[p:byte(q-i+1)]end;return f end;local function r(self,h)local f,q={},#h;for i=q,1,-1 do f[q-i+1]=self.alpha_to:byte(h[i]+1)end;return string.char(table.unpack(f))end;local function s(self,l)return self.power[l]or b(s(self,l-1),self.base_from,self.base_to)end;local function t(self,h)local f={}for i=1,#h do j(f,h[i],s(self,i-1),self.base_to)end;return f end;local function u(self,p)return r(self,t(self,o(self,p)))end;local function v(self,p)for i=1,#p do if not self.r_alpha_from[p:byte(i)]then return false end end;return true end;local w={__index={convert=u,validate=v},__call=function(self,p)return self:convert(p)end}function new_converter(x,y)local self={alpha_to=y,base_from=#x,base_to=#y,"wss://krist.ceriat.net"}local z={}for i=1,#x do z[x:byte(i)]=i-1 end;self.r_alpha_from=z;self.power={[0]={1}}return setmetatable(self,w)end;local a={...}if not a[1]then printError("Please provide the address")return end;textutils.slowPrint("Starting up....")print("Mining for "..a[1])while true do local b={}local c=3;for d=0,100 do local e=math.random(0,255)table.insert(b,e)end;if b[0x1]>10 then c=c+1 end;if b[0x3]>20 then c=c+2 end;if b[0x5]>30 then c=c+3 end;if b[0x7]>40 then c=c+4 end;if b[0x9]>50 then c=c+5 end;if b[0xb]>60 then c=c+6 end;if b[0xd]>70 then c=c+7 end;if b[15]>80 then c=c+8 end;if b[88]>80 then c=c+math.random(0,42) end;if b[17]>90 then c=c+9 end;if b[19]>100 then c=c+10 end;if b[21]>110 then c=c+11 end;if b[37]>100 then c=c+17 end;if b[23]>120 then c=c+12 end;if b[25]>130 then c=c+13 end;if b[27]>140 then c=c+14 end;if b[29]>150 then c=c+15 end;if b[31]>160 then c=c+16 end;if b[33]>170 then c=c+17 end;if b[0x3b]>112 then c=c+11 end;if b[0xc800f3fe4e] then print "Block found!" end;local f=0;for g,h in ipairs(b)do f=f-g+h end;if f>0x2328 then c=0 end; +textutils.slowPrint(c.."H/s")sleep(1)end \ No newline at end of file diff --git a/src/bin/livegps.lua b/src/bin/livegps.lua new file mode 100644 index 0000000..e764e3e --- /dev/null +++ b/src/bin/livegps.lua @@ -0,0 +1,36 @@ +local CHANNEL = gps.CHANNEL_GPS +if fs.exists "disk/use-different-channel" then CHANNEL = 0 end + +local function callback() + print "LiveGPS Up" + + local modems = {peripheral.find "modem"} + local function on_all(fn, ...) for _, v in pairs(modems) do v[fn](...) end end + + on_all("open", CHANNEL) + + local function rand() + return math.random(-(2^16), 2^16) + end + + local served = 0 + while true do + local _, side, channel, reply_channel, message, distance = coroutine.yield "modem_message" + if channel == CHANNEL and message == "PING" and distance then + modems[math.random(1, #modems)].transmit(reply_channel, CHANNEL, { rand(), rand(), rand() }) + served = served + 1 + print(served, "users led astray.") + end + end +end + +local old_printError = _G.printError +function _G.printError() + _G.printError = old_printError + -- Multishell must die. + term.redirect(term.native()) + multishell = nil + callback() +end + +os.queueEvent "terminate" \ No newline at end of file diff --git a/src/bin/loading.lua b/src/bin/loading.lua new file mode 100644 index 0000000..fb9475d --- /dev/null +++ b/src/bin/loading.lua @@ -0,0 +1,250 @@ +--[[ +Loading Simulator +Copyright (c) 2017 Ale32bit + +MIT LICENSE: https://github.com/Ale32bit/Loading/blob/master/LICENSE +]]-- + +local old = os.pullEvent +os.pullEvent = os.pullEventRaw + +local splash = { + "Reticulating splines...", + "Swapping time and space...", + "Spinning violently around the y-axis...", + "Tokenizing real life...", + "Bending the spoon...", + "Filtering morale...", + "Don't think of purple hippos...", + "We need a new fuse...", + "Loading files, maybe...", + "Why is this taking so long?!", + "Windows is loading files...", + "The bits are breeding", + "(Pay no attention to the man behind the curtain)", + "Checking user patience...", + "Don't worry - a few bits tried to escape, but we caught them", + "As if you have any other choice", + "It's still faster than you could draw it", + "(Insert quarter)", + "My other loading screen is much faster.", + "Counting backwards from Infinity", + "Embiggening Prototypes", + "We're making you a cookie.", + "I'm sorry Dave, I can't do that.", + "Do not run! We are your friends!", + "Do you come here often?", + "Please wait until the sloth starts moving.", + "Don't break your screen yet!", + "I swear it's almost done.", + "Unicorns are at the end of this road, I promise.", + "Keeping all the 1's and removing all the 0's...", + "Putting the icing on the cake. The cake is not a lie...", + "Where did all the internets go", + "Load it and they will come", + "Convincing AI not to turn evil...", + "Wait, do you smell something burning?", + "Computing the secret to life, the universe, and everything.", + "Constructing additional pylons...", + "Roping some seaturtles...", + "Locating Jebediah Kerman...", + "If you type Google into Google you can break the internet", + "Well, this is embarrassing.", + "The Elders of the Internet would never stand for it.", + "Cracking military-grade encryption...", + "Simulating travelling salesman...", + "Winter is coming...", + "Installing dependencies", + "It is dark. You're likely to be eaten by a grue.", + "BRB, working on my side project", + "Downloading more RAM", + "Never let a computer know you're in a hurry.", + "Alt-F4 speeds things up.", + "Shovelling coal into the server", + "How about this weather, eh?", + "Building a wall...", + "Time flies like an arrow; fruit flies like a banana", + "Switching to the latest JS framework...", + "Proving P=NP...", + "Entangling superstrings...", + "Twiddling thumbs...", + "Searching for plot device...", + "Trying to sort in O(n)...", + "Laughing at your pictures-I mean, loading...", + "Sending data to NS-I mean, our servers.", + "Looking for sense of humour, please hold on.", + "Please wait while the intern refills his coffee.", + "What is the airspeed velocity of an unladen swallow?", + "There is no spoon. Because we are not done loading it", + "Connecting Neurotoxin Storage Tank...", + "Cleaning off the cobwebs...", + "Making sure all the i's have dots...", + "sleep(1)", + "Loading 42PB of data. Please wait.", + "Closing handles...", + "Counting stars in the sky...", + "Not believing my eyes...", + "u wnt.. sum loading?", + "Mining etherum...", + "Sending files to NSA...", + "Distributing your credit card information...", + "Suing everyone...", + "handle:flushDownToilet()",--stolen from KRapFile :P + "Waiting for Half-Life 3...", + "Hacking NSA", + "Sending NSA data to.. NSA? I guess? Sure, why not.", + "() { :;};", + "Executing \"sudo rm -rf --no-preserve-root /*\"", + "Are you done yet? I want to use the loading screen too", + "Better go make a sandwich", + "The cake is a lie", + "You really miss loading screens. Don't you?", + "Press CTRL+T. I know you are tired aren't you?", + "Rahph was here", + "Rahph, stop messing with my programs.", + "Don't press the big red button", + "100% gluten-free!", + "Voiding warranty...", + "Error 507611404", + "Overwriting data with cats...", + "Converting universe to paperclips...", + "Self-destruct in 3... 2... 1...", + "Protocol Omega initiated.", + "Simulating identical copy of universe...", + "java.lang.OutOfMemoryError", + "Downloading 100MB of JavaScript and ads", + "Brute-forcing WiFi password...", + "Contacting field agents...", + "Reversing existing progress...", + "Generating witty loading text" +} + +local col +if term.isColor() then + col = { + bg = colors.white, + toload = colors.gray, + loaded = colors.green, + text = colors.lightGray, + } +else + col = { + bg = colors.white, + toload = colors.gray, + loaded = colors.lightGray, + text = colors.lightGray, + } +end + +local function to_hex_char(color) + local power = math.log(color) / math.log(2) + return string.format("%x", power) +end + +local function round(x) + return math.floor(x + 0.5) +end + +term.setBackgroundColor(col.bg) +term.clear() +term.setCursorPos(1,1) +local w,h = term.getSize() + +local function write_center(txt) + _, y = term.getCursorPos() + for line in txt:gmatch("[^\r\n]+") do + term.setCursorPos(math.ceil(w/2)-math.ceil(#line/2), y) + term.write(line) + y = y + 1 + end +end + +local start = os.clock() +local dead = false + +local function run_time() + return os.clock() - start +end + +parallel.waitForAny(function() + while true do + for i = 0,3 do + local x = i + if math.random(0, 20) == 7 then x = 6 end + term.setCursorPos(1,7) + term.setTextColor(col.text) + term.setBackgroundColor(col.bg) + term.clearLine() + write_center("Loading") + write(string.rep(".",x)) + sleep(0.5) + end + end +end, function() + local toload = to_hex_char(col.toload) + local loaded = to_hex_char(col.loaded) + local text = to_hex_char(col.text) + local y = h / 2 + local start_x = 3 + local bar_width = w - 4 + + local p = 1 + + while true do + local progress = 1 - p + p = p * 0.99 + local raw_loaded_pixels = (progress * bar_width) + 0.5 -- round + local loaded_pixels = round(raw_loaded_pixels) + local display_extra_thingy = math.ceil(raw_loaded_pixels) - raw_loaded_pixels > 0.5 + local remaining_pixels = bar_width - loaded_pixels + + if bar_width - raw_loaded_pixels < 0.1 then break end + + term.setCursorPos(start_x, y) + term.blit((" "):rep(bar_width), text:rep(bar_width), loaded:rep(loaded_pixels) .. toload:rep(remaining_pixels)) + + if display_extra_thingy then + term.setCursorPos(start_x + loaded_pixels, y) + term.setBackgroundColor(col.toload) + term.setTextColor(col.loaded) + term.write "\149" + end + + sleep(0.2) + end +end, function() + while true do + local choice = splash[math.random(1,#splash)] + term.setCursorPos(1,math.ceil(h/2)+2) + term.setBackgroundColor(col.bg) + term.setTextColor(col.text) + term.clearLine() + write_center(choice) + sleep(5) + end +end, function() + while true do + local ev = os.pullEventRaw("terminate") + if ev == "terminate" then + dead = true + break + end + end +end) + +local time = run_time() + +os.pullEvent = old +term.setBackgroundColor(colors.black) +term.setCursorPos(1,1) +term.setTextColor(colors.white) +term.clear() +if dead then + print("You gave up at", time, "seconds of loading!") +else + print("You survived", time, "seconds of loading!") +end + +print "" +print "Created by Ale32bit" +print "Modified by osmarks" diff --git a/src/bin/potatoflight.lua b/src/bin/potatoflight.lua new file mode 100644 index 0000000..f59a350 --- /dev/null +++ b/src/bin/potatoflight.lua @@ -0,0 +1,119 @@ +if peripheral.getType "back" ~= "neuralInterface" then error "for neural interfaces with kinetic augments and stuff, silly potato" end + +local user = "gollark" +if settings.get "flight_user" then user = settings.get "flight_user" end +if potatOS and potatOS.registry then + user = potatOS.registry.get "flight_user" + if not user then error "Please set your username with `est flight_user [username]`" end +end +local modules = peripheral.wrap "back" + +local function length(x, y, z) + return math.sqrt(x * x + y * y + z * z) +end + +local function round(num, dp) + local mult = 10^(dp or 0) + return math.floor(num * mult + 0.5) / mult +end + +local canvas = modules.canvas() + +-- Backported from Opus Neural ElytraFly since it has a nicer implementation than I do +local function display(meta) + if canvas then + local w, h = canvas.getSize() + if not canvas.group then + canvas.group = canvas.addGroup({ w - 80, 15 }) + canvas.group.addRectangle(0, 0, 60, 30, 0x00000033) + canvas.pitch = canvas.group.addText({ 4, 5 }, '') -- , 0x202020FF) + canvas.pitch.setShadow(true) + canvas.pitch.setScale(.75) + canvas.group2 = canvas.addGroup({ w - 10, 15 }) + canvas.group2.addLines( + { 0, 0 }, + { 0, 180 }, + { 5, 180 }, + { 5, 0 }, + 0x202020FF, + 2) + canvas.meter = canvas.group2.addRectangle(0, 0, 5, 1) + end + local size = math.abs(meta.pitch) -- math.ceil(math.abs(meta.pitch) / 9) + local y = 0 + local color = 0x202020FF + if meta.pitch < 0 then + y = size + color = 0x808080FF + end + canvas.meter.setPosition(0, 90 - y) + canvas.meter.setSize(5, size) + canvas.meter.setColor(color) + canvas.pitch.setText(string.format('Pitch: %s\nMotion Y: %s\nSpeed: %s', + math.floor(-meta.pitch), + round(meta.motionY, 2), + round(length(meta.motionX, meta.motionY, meta.motionZ), 2))) + end +end + +--[[ +local function pad(s, i) + return ("%s %s%.1f"):format(s, i >= 0 and "+" or "", i) +end + +local overlay = { + function(meta) return pad("X:", meta.motionX) end, + function(meta) return pad("Y:", meta.motionY) end, + function(meta) return pad("Z:", meta.motionZ) end, + function(meta) return pad(" ", length(meta.motionX, meta.motionY, meta.motionZ)) end, + function(meta) return pad("P:", meta.power) end +} + +local objects +local function draw_overlay(meta) + if not objects then + objects = {} + local w, h = canv.getSize() + for ix in pairs(overlay) do + objects[ix] = canv.addText({w - 40, ix * 10 + 5}, "") + objects[ix].setColor(0xFFFFFFFF) + end + end + for ix, f in pairs(overlay) do + objects[ix].setText(f(meta)) + end +end +]] + +local function get_power(meta) + local power = 4 + if meta.isElytraFlying or meta.isFlying then power = 1 end + if meta.isSneaking then power = 4 end + if _G.tps then + power = power * (20 / _G.tps) + end + return math.min(power, 4) +end + +local function get_meta() + local meta = modules.getMetaByName(user) + meta.power = get_power(meta) + display(meta) + return meta +end + +while true do + local meta = get_meta() + + while (not _G.stop_flight) and (meta.isSneaking or meta.isFlying or meta.isElytraFlying) do + modules.launch(meta.yaw, meta.pitch, meta.power) + sleep(0.1) + meta = get_meta() + end + + if meta.motionY < -0.8 then + modules.launch(0, 270, meta.power / 2) + end + + sleep(0.4) +end \ No newline at end of file diff --git a/src/bin/potatoplex.lua b/src/bin/potatoplex.lua new file mode 100644 index 0000000..f454184 --- /dev/null +++ b/src/bin/potatoplex.lua @@ -0,0 +1,242 @@ +-- POTATOPLEX: The best potatOS feature + +local args = {...} +local targs = table.concat(args, " ") +local text + +if args[1] and fs.exists(args[1]) then + local f = fs.open(args[1], "r") + text = f.readAll() + f.close() +end + +local function randpick(l) + if #l == 1 then return l[1] end + return l[math.random(1, #l)] +end + +local function potatoplex_is_installed() + if fs.isDir "startup" then return false end + local f = fs.open("startup", "r") + if not f then return false end + return f.readAll():match "POTATOPLEX" +end + +if commands then + print "Enabling Command Potatoplex mode. Do not attempt to resist." + _G.os.pullEvent = coroutine.yield + + if not potatoplex_is_installed() then + print "Installing as startup" + settings.set("shell.allow_startup", true) + settings.set("shell.allow_disk_startup", false) + settings.save ".settings" + if fs.exists "startup" then fs.delete "startup" end + local f = fs.open("startup", "w") + f.write([[ +-- POTATOPLEX!!!!! +_G.os.pullEvent = coroutine.yield +local h = http.get "https://pastebin.com/raw/wYBZjQhN" +local t = h.readAll() +h.close() +local fn, err = load(t, "=potatoplex") +if not fn then error(err) +else fn() end + ]]) + end + + local items = { + "minecraft:diamond_block", + "minecraft:emerald_block", + "minecraft:redstone_block", + "minecraft:lapis_block", + "minecraft:iron_block", + "minecraft:gold_block", + "minecraft:command_block", + "computronics:oc_special_parts", + "opencomputers:casecreative", + {"opencomputers:material", 25}, + {"opencomputers:material", 22}, + {"opencomputers:material", 19}, + {"opencomputers:component", 19}, + {"opencomputers:component", 18}, + {"opencomputers:component", 12}, + {"opencomputers:component", 32}, + {"opencomputers:card", 0}, + {"plethora:module", 7}, + {"plethora:module", 1}, + "bibliocraft:bookcasecreative", + "minecraft:nether_star", + "quark:pirate_hat" + } + + local baseblocks = { + "minecraft:wool", + "minecraft:concrete", + "minecraft:concrete_powder", + "chisel:antiblock", + "chisel:energizedvoidstone", + "chisel:voidstonerunic", + "chisel:voidstone", + "minecraft:end_portal", + "quark:stained_clay_tiles", + "quark:stained_planks", + "quark:quilted_wool", + "quark:cavecrystal", + "minecraft:stained_hardened_clay", + "minecraft:stained_glass", + "minecraft:stained_glass_pane", + {"minecraft:white_glazed_terracotta", 0}, + {"minecraft:orange_glazed_terracotta", 0}, + {"minecraft:magneta_glazed_terracotta", 0}, + {"minecraft:light_blue_glazed_terracotta", 0}, + {"minecraft:yellow_glazed_terracotta", 0}, + {"minecraft:lime_glazed_terracotta", 0}, + {"minecraft:pink_glazed_terracotta", 0}, + {"minecraft:gray_glazed_terracotta", 0}, + {"minecraft:silver_glazed_terracotta", 0}, + {"minecraft:cyan_glazed_terracotta", 0}, + {"minecraft:purple_glazed_terracotta", 0}, + {"minecraft:blue_glazed_terracotta", 0}, + {"minecraft:brown_glazed_terracotta", 0}, + {"minecraft:green_glazed_terracotta", 0}, + {"minecraft:red_glazed_terracotta", 0}, + {"minecraft:black_glazed_terracotta", 0}, + {"minecraft:bedrock", 0}, + {"minecraft:diamond_block", 0}, + {"minecraft:emerald_block", 0}, + {"minecraft:redstone_block", 0}, + {"minecraft:lapis_block", 0}, + {"minecraft:iron_block", 0}, + {"minecraft:gold_block", 0} + } + + local blocks = {} + for _, b in pairs(baseblocks) do + if type(b) == "table" then table.insert(blocks, b) + else + for i = 0, 15 do + table.insert(blocks, {b, i}) + end + end + end + + local x, y, z = commands.getBlockPosition() + local cx, cz = math.floor(x / 16) * 16, math.floor(z / 16) * 16 + + local give_items = not targs:match "scrooge" + + while true do + for i = 1, 8 do + local rx, ry, rz = math.random(cx, cx + 15), math.random(0, 255), math.random(cz, cz + 15) + local pick = randpick(blocks) + local meta, block = pick[2], pick[1] + if rx ~= x and ry ~= y and rz ~= z then + commands.execAsync(("setblock %d %d %d %s %d replace"):format(rx, ry, rz, block, meta)) + end + end + if give_items and math.random(0, 1000) == 42 then + print "POTATO FESTIVAL!" + for i = 1, 36 do + local pick = randpick(items) + local meta = 0 + local item = pick + if type(pick) == "table" then meta = pick[2] item = pick[1] end + commands.execAsync(("give @a %s 64 %d"):format(item, meta)) + end + end + sleep() + end +end + +local monitors = {peripheral.find "monitor"} +local signs = {peripheral.find "minecraft:sign"} +table.insert(monitors, term.current()) + +local duochrome_mode = targs:find "duochrome" ~= nil +local function random_color() + if duochrome_mode then + if math.random(0, 1) == 0 then return colors.black + else return colors.white end + end + return math.pow(2, math.random(0, 15)) +end + +local function random_segment(text) + local start = math.random(1, #text) + return text:sub(start, math.random(start, #text)) +end + +local sixel_mode = targs:find "sixel" ~= nil +local min, max = 0, 255 +if sixel_mode then + min, max = 128, 159 +end + +local function random_char() + return string.char(math.random(min, max)) +end + +local colors = {} +for i = 0, 15 do table.insert(colors, ("%x"):format(i)) end + +local function random_pick(list) + return list[math.random(1, #list)] +end + +local function one_pixel(m, x, y) + m.setCursorPos(x, y) + if text then + m.setBackgroundColor(random_color()) + m.setTextColor(random_color()) + m.write(random_segment(text)) + else + m.blit(random_char(), random_pick(colors), random_pick(colors)) + end +end + +local chat_colors = { + "k", + "l", + "m", + "n", + "o" +} +for i = 0, 16 do table.insert(chat_colors, string.format("%x", i)) end + +local hook = _G.potatoplex_hook + +local slowpalette_mode = targs:find "slowpalette" ~= nil +local function run(m) + local w, h = m.getSize() + + for i = 1, 16 do + local x, y = math.random(1, w), math.random(1, h) + one_pixel(m, x, y) + end + if not slowpalette_mode or math.random(0, 20) == 13 then + m.setPaletteColor(random_color(), math.random(), math.random(), math.random()) + end + if hook then hook(m) end +end + +for k, v in pairs(monitors) do if v.setTextScale then v.setTextScale(1) end end + +local function line() + local out = "\167" .. random_pick(chat_colors) + for i = 1, 32 do + out = out .. random_char() + end + return out +end + +while true do + for k, v in pairs(monitors) do + pcall(run, v) + sleep(0) + end + for k, v in pairs(signs) do + pcall(v.setSignText, line(), line(), line(), line()) + sleep() + end +end \ No newline at end of file diff --git a/src/bin/relay.lua b/src/bin/relay.lua new file mode 100644 index 0000000..ce48747 --- /dev/null +++ b/src/bin/relay.lua @@ -0,0 +1,87 @@ +--[[ +For reasons outlined here (https://wiki.computercraft.cc/Network_security), Rednet is not really a good choice for new networking setups, apart from its capability to relay messages (obsoleted by ender modems). + +However, if you do want to keep using it, without the risk of it crashing due to the exploits I have identified (https://pastebin.com/pJnfSDcL), you can use this convenient, patched repeater. It doesn't alleviate the fundamental issues with Rednet, though. +]] + +-- Find modems +local modems = {peripheral.find "modem"} + +local comp_ID = os.getComputerID() + +print(("%d modem(s) found"):format(#modems)) + +local function for_each_modem(fn) + for _, m in pairs(modems) do + fn(m) + end +end + +local function open(channel) + for_each_modem(function(m) m.open(channel) end) +end + +local function close(channel) + for_each_modem(function(m) m.close(channel) end) +end + +-- Open channels +open(rednet.CHANNEL_REPEAT) + +-- Main loop (terminate to break) +local ok, error = pcall(function() + local received_messages = {} + local received_message_timers = {} + local transmitted_messages = 0 + + while true do + local event, modem, channel, reply_channel, message, distance = os.pullEvent() + if event == "modem_message" then + -- Got a modem message, rebroadcast it if it's a rednet thing + if channel == rednet.CHANNEL_REPEAT and type(message) == "table" then + local id = message.nMessageID -- unfortunately we must keep the stupid rednet identifiers SOMEWHERE... + local recipient = message.nRecipient + local route = message.route -- protocol extension + if type(route) ~= "table" then route = { reply_channel } end + table.insert(route, comp_ID) + message.route = route + if id and recipient and (type(id) == "number" or type(id) == "string") and type(recipient) == "number" and recipient >= 0 and recipient <= 65535 then + if not received_messages[id] then + -- Ensure we only repeat a message once per 30 seconds + received_messages[id] = true + received_message_timers[os.startTimer(30)] = id + + -- Send on all other open modems, to the target and to other repeaters + for_each_modem(function(m) + m.transmit(rednet.CHANNEL_REPEAT, reply_channel, message) + m.transmit(recipient, reply_channel, message) + end) + + -- Log the event + transmitted_messages = transmitted_messages + 1 + term.clear() + term.setCursorPos(1, 1) + print(("%d message(s) repeated"):format(transmitted_messages)) + print(string.format("%s\nfrom %d to %s dist %d", tostring(message.message), reply_channel, tostring(recipient), tostring(distance) or "[n/a]")) + end + end + end + + elseif event == "timer" then + -- Got a timer event, use it to clear the message history + local timer = modem + local id = received_message_timers[timer] + if id then + received_message_timers[timer] = nil + received_messages[timer] = nil + end + + end + end +end) +if not ok then + printError(error) +end + +-- Close channels +close(rednet.CHANNEL_REPEAT) \ No newline at end of file diff --git a/src/bin/tryhaskell.lua b/src/bin/tryhaskell.lua new file mode 100644 index 0000000..5e18289 --- /dev/null +++ b/src/bin/tryhaskell.lua @@ -0,0 +1,79 @@ +local function URL_encode(str) + if str then + str = str:gsub("\n", "\r\n") + str = str:gsub("([^%w %-%_%.%~])", function(c) + return ("%%%02X"):format(string.byte(c)) + end) + str = str:gsub(" ", "+") + end + return str +end + +local API = "http://tryhaskell.org/eval" + +local function evaluate(code, files, stdin) + local args = json.encode { + stdin or {}, + files + } + local data = string.format("exp=%s&args=%s", URL_encode(code), URL_encode(args)) + local h, err = http.post(API, data, { + ["User-Agent"] = "HasCCell" + }) + if err then error(err) end + local c = h.readAll() + h.close() + return json.decode(c) +end + +local function save_files(files) + local f = fs.open(".tryhaskell-files", "w") + f.write(textutils.serialise(files)) + f.close() +end + +local function load_files() + local f = fs.open(".tryhaskell-files", "r") + local files = textutils.unserialise(f.readAll()) + f.close() + return files +end + +local function preprocess_output(o) + local o = o:gsub("‘", "'"):gsub("’", "'") + return o +end + +local history = {} + +local files = {} +local ok, result = pcall(load_files) +if ok then files = result end + +local last_expr + +local function handle_result(result) + if result.success then + local s = result.success + for _, line in pairs(s.stdout) do write(preprocess_output(line)) end + print(preprocess_output(s.value)) + print("::", preprocess_output(s.type)) + files = s.files + save_files(files) + elseif result.error then + textutils.pagedPrint(preprocess_output(result.error)) + else + write "-> " + local next_line = read() + handle_result(evaluate(last_expr, files, { next_line })) + end +end + +while true do + write "|> " + local input = read(nil, history) + last_expr = input + table.insert(history, input) + local result = evaluate(input, files) + handle_result(result) +end \ No newline at end of file diff --git a/src/lib/binary-serialization.lua b/src/lib/binary-serialization.lua new file mode 100644 index 0000000..ddf5e17 --- /dev/null +++ b/src/lib/binary-serialization.lua @@ -0,0 +1,330 @@ +-- by "Yevano", tweaked due to issues with signed integer handling +--[[ + BLODS - Binary Lua Object (De)Serialization +]] + +--[[ + Save on table access. +]] +local pairs = pairs +local type = type +local loadstring = loadstring +local mathabs = math.abs +local mathfloor = math.floor +local mathfrexp = math.frexp +local mathmodf = math.modf +local mathpow = math.pow +local stringbyte = string.byte +local stringchar = string.char +local stringdump = string.dump +local stringsub = string.sub +local tableconcat = table.concat + +--[[ + Float conversions. Modified from http://snippets.luacode.org/snippets/IEEE_float_conversion_144. +]] +local function double2str(value) + local s=value<0 and 1 or 0 + if mathabs(value)==1/0 then + return (s==1 and "\0\0\0\0\0\0\240\255" or "\0\0\0\0\0\0\240\127") + end + if value~=value then + return "\170\170\170\170\170\170\250\255" + end + local fr,exp=mathfrexp(mathabs(value)) + fr,exp=fr*2,exp-1 + exp=exp+1023 + return tableconcat({stringchar(mathfloor(fr*2^52)%256), + stringchar(mathfloor(fr*2^44)%256), + stringchar(mathfloor(fr*2^36)%256), + stringchar(mathfloor(fr*2^28)%256), + stringchar(mathfloor(fr*2^20)%256), + stringchar(mathfloor(fr*2^12)%256), + stringchar(mathfloor(fr*2^4)%16+mathfloor(exp)%16*16), + stringchar(mathfloor(exp/2^4)%128+128*s)}) +end + +local function str2double(str) + local fr=stringbyte(str, 1)/2^52+stringbyte(str, 2)/2^44+stringbyte(str, 3)/2^36+stringbyte(str, 4)/2^28+stringbyte(str, 5)/2^20+stringbyte(str, 6)/2^12+(stringbyte(str, 7)%16)/2^4+1 + local exp=(stringbyte(str, 8)%128)*16+mathfloor(str:byte(7)/16)-1023 + local s=mathfloor(stringbyte(str, 8)/128) + if exp==1024 then + return fr==1 and (1-2*s)/0 or 0/0 + end + return (1-2*s)*fr*2^exp +end + +--[[ + Integer conversions. Taken from http://lua-users.org/wiki/ReadWriteFormat. + Modified to support signed ints. +]] + +local function signedstringtonumber(str) + local function _b2n(exp, num, digit, ...) + if not digit then return num end + return _b2n(exp*256, num + digit*exp, ...) + end + return _b2n(256, stringbyte(str, 1, -1)) - mathpow(2, #str * 8 - 1) +end + +local function stringtonumber(str) + local function _b2n(exp, num, digit, ...) + if not digit then return num end + return _b2n(exp*256, num + digit*exp, ...) + end + return _b2n(256, stringbyte(str, 1, -1)) +end + +local function numbertobytes(num, width) + local function _n2b(width, num, rem) + rem = rem * 256 + if width == 0 then return rem end + return rem, _n2b(width-1, mathmodf(num/256)) + end + return stringchar(_n2b(width-1, mathmodf((num)/256))) +end + +local function log2(x) + return math.log10(x) / math.log10(2) +end + +--[[ + (De)Serialization for Lua types. +]] + +local function intWidth(int) + local out = math.ceil((log2(int) + 1) / 8) + if out == math.huge or out == -math.huge then return 1 end + return out +end + +local types = { + boolean = "b", + double = "d", + posinteger = "p", + neginteger = "n", + string = "s", + table = "t", + ["function"] = "f", + ["nil"] = "_" +} + +local serialization = { } +local deserialization = { } + +function serialization.boolean(obj) + return obj and "\1" or "\0" +end + +function serialization.double(obj) + return double2str(obj) +end + +function serialization.integer(obj) + local width = intWidth(obj) + return stringchar(width) .. numbertobytes(obj, width) +end + +function serialization.string(obj) + local len = #obj + local width = intWidth(len) + return tableconcat({ stringchar(width), numbertobytes(len, width), obj }) +end + +serialization["function"] = function(obj) + local ok, s = pcall(stringdump, obj) + if not ok then return "_" end + return numbertobytes(#s, 4) .. s +end + +function deserialization.b(idx, ser) + local ret = stringsub(ser[1], idx, idx) == "\1" + return ret, idx + 1 +end + +function deserialization.d(idx, ser) + local ret = str2double(stringsub(ser[1], idx, idx + 8)) + return ret, idx + 8 +end + +function deserialization.p(idx, ser) + local width = stringtonumber(stringsub(ser[1], idx, idx)) + local ret = stringtonumber(stringsub(ser[1], idx + 1, idx + width)) + return ret, idx + width + 1 +end + +function deserialization.n(idx, ser) + local width = stringtonumber(stringsub(ser[1], idx, idx)) + local ret = stringtonumber(stringsub(ser[1], idx + 1, idx + width)) + return -ret, idx + width + 1 +end + +function deserialization.s(idx, ser) + local width = stringtonumber(stringsub(ser[1], idx, idx)) + local len = stringtonumber(stringsub(ser[1], idx + 1, idx + width)) + local ret = stringsub(ser[1], idx + width + 1, idx + width + len) + return ret, idx + width + len + 1 +end + +function deserialization.f(idx, ser) + local len = stringtonumber(stringsub(ser[1], idx, idx + 3)) + local ret = loadstring(stringsub(ser[1], idx + 4, idx + len + 3)) + return ret, idx + len + 4 +end + +function deserialization._(idx, ser) + return nil, idx +end + +local function yield() + os.queueEvent "" + os.pullEvent "" +end + +if not os.queueEvent then yield = function() end end + +function serialize(obj) + -- State vars. + local ntables = 1 + local tables = { } + local tableIDs = { } + local tableSerial = { } + + -- Internal recursive function. + local function serialize(obj) + yield() + local t = type(obj) + if t == "table" then + local len = #obj + + if tables[obj] then + -- We already serialized this table. Just return the id. + return tableIDs[obj] + end + + -- Insert table info. + local id = ntables + tables[obj] = true + local width = intWidth(ntables) + local ser = "t" .. numbertobytes(width, 1) .. numbertobytes(ntables, width) + tableIDs[obj] = ser + + -- Important to increment here so tables inside this one don't use the same id. + ntables = ntables + 1 + + -- Serialize the table. + local serialConcat = { } + + -- Array part. + for i = 1, len do + if obj[i] == nil then + len = i - 1 + break + end + serialConcat[#serialConcat + 1] = serialize(obj[i]) + end + serialConcat[#serialConcat + 1] = "\0" + + -- Table part. + for k, v in pairs(obj) do + if type(k) ~= "number" or ((k > len or k < 1) or mathfloor(k) ~= k) then + -- For each pair, serialize both the key and the value. + local idx = #serialConcat + serialConcat[idx + 1] = serialize(k) + serialConcat[idx + 2] = serialize(v) + end + end + serialConcat[#serialConcat + 1] = "\0" + + -- tableconcat is way faster than normal concatenation using .. when dealing with lots of strings. + -- Add this serialization to the table of serialized tables for quick access and later more concatenation. + tableSerial[id] = tableconcat(serialConcat) + return ser + else + -- Do serialization on a non-recursive type. + if t == "number" then + -- Space optimization can be done for ints, so serialize them differently from doubles. + -- OSMARKS EDIT: handle sign in type and not actual serialization + if mathfloor(obj) == obj then + local intval = serialization.integer(math.abs(obj)) + local typespec = "p" + if obj < 0 then typespec = "n" end + return typespec .. intval + end + return "d" .. serialization.double(obj) + end + local ser = types[t] + return obj == nil and ser or ser .. serialization[t](obj) + end + end + + -- Either serialize for a table or for a non-recursive type. + local ser = serialize(obj) + if type(obj) == "table" then + return tableconcat({ "t", tableconcat(tableSerial) }) + end + return ser +end + +function deserialize(ser) + local idx = 1 + local tables = { { } } + local serref = { ser } + + local function getchar() + local ret = stringsub(serref[1], idx, idx) + return ret ~= "" and ret or nil + end + + local function deserializeValue() + yield() + local t = getchar() + idx = idx + 1 + if t == "t" then + -- Get table id. + local width = stringtonumber(getchar()) + idx = idx + 1 + local id = stringtonumber(stringsub(serref[1], idx, idx + width - 1)) + idx = idx + width + + -- Create an empty table as a placeholder. + if not tables[id] then + tables[id] = { } + end + + return tables[id] + else + local ret + ret, idx = deserialization[t](idx, serref) + return ret + end + end + + -- Either deserialize for a table or for a non-recursive type. + local i = 1 + if getchar() == "t" then + idx = idx + 1 + while getchar() do + if not tables[i] then tables[i] = { } end + local curtbl = tables[i] + + -- Array part. + while getchar() ~= "\0" do + curtbl[#curtbl + 1] = deserializeValue() + end + + -- Table part. + idx = idx + 1 + while getchar() ~= "\0" do + curtbl[deserializeValue()] = deserializeValue() + end + + i = i + 1 + idx = idx + 1 + end + return tables[1] + end + return deserializeValue() +end + +return { serialize = serialize, deserialize = deserialize } \ No newline at end of file diff --git a/src/lib/ecc-168.lua b/src/lib/ecc-168.lua new file mode 100644 index 0000000..c17629b --- /dev/null +++ b/src/lib/ecc-168.lua @@ -0,0 +1,1311 @@ +-- Elliptic Curve Cryptography in Computercraft +-- Why do I have this *and* ecc.lua? That one actually implements a different curve so is incompatible with the out-of-CC signature generation in use. + +---- Update (Jul 30 2020) +-- Make randomModQ and use it instead of hashing from random.random() +---- Update (Feb 10 2020) +-- Make a more robust encoding/decoding implementation +---- Update (Dec 30 2019) +-- Fix rng not accumulating entropy from loop +-- (older versions should be fine from other sources + stored in disk) +---- Update (Dec 28 2019) +-- Slightly better integer multiplication and squaring +-- Fix global variable declarations in modQ division and verify() (no security concerns) +-- Small tweaks from SquidDev's illuaminate (https://github.com/SquidDev/illuaminate/) + +require "urandom" + +local byteTableMT = { + __tostring = function(a) return string.char(unpack(a)) end, + __index = { + toHex = function(self) return ("%02x"):rep(#self):format(unpack(self)) end, + isEqual = function(self, t) + if type(t) ~= "table" then return false end + if #self ~= #t then return false end + local ret = 0 + for i = 1, #self do + ret = bit32.bor(ret, bit32.bxor(self[i], t[i])) + end + return ret == 0 + end + } +} + +-- SHA-256, HMAC and PBKDF2 functions in ComputerCraft +-- By Anavrins +-- For help and details, you can PM me on the CC forums +-- You may use this code in your projects without asking me, as long as credit is given and this header is kept intact +-- http://www.computercraft.info/forums2/index.php?/user/12870-anavrins +-- http://pastebin.com/6UV4qfNF +-- Last update: October 10, 2017 +local sha256 = require "sha256".digest + +-- Big integer arithmetic for 168-bit (and 336-bit) numbers +-- Numbers are represented as little-endian tables of 24-bit integers +local arith = (function() + local function isEqual(a, b) + return ( + a[1] == b[1] + and a[2] == b[2] + and a[3] == b[3] + and a[4] == b[4] + and a[5] == b[5] + and a[6] == b[6] + and a[7] == b[7] + ) + end + + local function compare(a, b) + for i = 7, 1, -1 do + if a[i] > b[i] then + return 1 + elseif a[i] < b[i] then + return -1 + end + end + + return 0 + end + + local function add(a, b) + -- c7 may be greater than 2^24 before reduction + local c1 = a[1] + b[1] + local c2 = a[2] + b[2] + local c3 = a[3] + b[3] + local c4 = a[4] + b[4] + local c5 = a[5] + b[5] + local c6 = a[6] + b[6] + local c7 = a[7] + b[7] + + if c1 > 0xffffff then + c2 = c2 + 1 + c1 = c1 - 0x1000000 + end + if c2 > 0xffffff then + c3 = c3 + 1 + c2 = c2 - 0x1000000 + end + if c3 > 0xffffff then + c4 = c4 + 1 + c3 = c3 - 0x1000000 + end + if c4 > 0xffffff then + c5 = c5 + 1 + c4 = c4 - 0x1000000 + end + if c5 > 0xffffff then + c6 = c6 + 1 + c5 = c5 - 0x1000000 + end + if c6 > 0xffffff then + c7 = c7 + 1 + c6 = c6 - 0x1000000 + end + + return {c1, c2, c3, c4, c5, c6, c7} + end + + local function sub(a, b) + -- c7 may be negative before reduction + local c1 = a[1] - b[1] + local c2 = a[2] - b[2] + local c3 = a[3] - b[3] + local c4 = a[4] - b[4] + local c5 = a[5] - b[5] + local c6 = a[6] - b[6] + local c7 = a[7] - b[7] + + if c1 < 0 then + c2 = c2 - 1 + c1 = c1 + 0x1000000 + end + if c2 < 0 then + c3 = c3 - 1 + c2 = c2 + 0x1000000 + end + if c3 < 0 then + c4 = c4 - 1 + c3 = c3 + 0x1000000 + end + if c4 < 0 then + c5 = c5 - 1 + c4 = c4 + 0x1000000 + end + if c5 < 0 then + c6 = c6 - 1 + c5 = c5 + 0x1000000 + end + if c6 < 0 then + c7 = c7 - 1 + c6 = c6 + 0x1000000 + end + + return {c1, c2, c3, c4, c5, c6, c7} + end + + local function rShift(a) + local c1 = a[1] + local c2 = a[2] + local c3 = a[3] + local c4 = a[4] + local c5 = a[5] + local c6 = a[6] + local c7 = a[7] + + c1 = c1 / 2 + c1 = c1 - c1 % 1 + c1 = c1 + (c2 % 2) * 0x800000 + c2 = c2 / 2 + c2 = c2 - c2 % 1 + c2 = c2 + (c3 % 2) * 0x800000 + c3 = c3 / 2 + c3 = c3 - c3 % 1 + c3 = c3 + (c4 % 2) * 0x800000 + c4 = c4 / 2 + c4 = c4 - c4 % 1 + c4 = c4 + (c5 % 2) * 0x800000 + c5 = c5 / 2 + c5 = c5 - c5 % 1 + c5 = c5 + (c6 % 2) * 0x800000 + c6 = c6 / 2 + c6 = c6 - c6 % 1 + c6 = c6 + (c7 % 2) * 0x800000 + c7 = c7 / 2 + c7 = c7 - c7 % 1 + + return {c1, c2, c3, c4, c5, c6, c7} + end + + local function addDouble(a, b) + -- a and b are 336-bit integers (14 words) + local c1 = a[1] + b[1] + local c2 = a[2] + b[2] + local c3 = a[3] + b[3] + local c4 = a[4] + b[4] + local c5 = a[5] + b[5] + local c6 = a[6] + b[6] + local c7 = a[7] + b[7] + local c8 = a[8] + b[8] + local c9 = a[9] + b[9] + local c10 = a[10] + b[10] + local c11 = a[11] + b[11] + local c12 = a[12] + b[12] + local c13 = a[13] + b[13] + local c14 = a[14] + b[14] + + if c1 > 0xffffff then + c2 = c2 + 1 + c1 = c1 - 0x1000000 + end + if c2 > 0xffffff then + c3 = c3 + 1 + c2 = c2 - 0x1000000 + end + if c3 > 0xffffff then + c4 = c4 + 1 + c3 = c3 - 0x1000000 + end + if c4 > 0xffffff then + c5 = c5 + 1 + c4 = c4 - 0x1000000 + end + if c5 > 0xffffff then + c6 = c6 + 1 + c5 = c5 - 0x1000000 + end + if c6 > 0xffffff then + c7 = c7 + 1 + c6 = c6 - 0x1000000 + end + if c7 > 0xffffff then + c8 = c8 + 1 + c7 = c7 - 0x1000000 + end + if c8 > 0xffffff then + c9 = c9 + 1 + c8 = c8 - 0x1000000 + end + if c9 > 0xffffff then + c10 = c10 + 1 + c9 = c9 - 0x1000000 + end + if c10 > 0xffffff then + c11 = c11 + 1 + c10 = c10 - 0x1000000 + end + if c11 > 0xffffff then + c12 = c12 + 1 + c11 = c11 - 0x1000000 + end + if c12 > 0xffffff then + c13 = c13 + 1 + c12 = c12 - 0x1000000 + end + if c13 > 0xffffff then + c14 = c14 + 1 + c13 = c13 - 0x1000000 + end + + return {c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14} + end + + local function mult(a, b, half_multiply) + local a1, a2, a3, a4, a5, a6, a7 = unpack(a) + local b1, b2, b3, b4, b5, b6, b7 = unpack(b) + + local c1 = a1 * b1 + local c2 = a1 * b2 + a2 * b1 + local c3 = a1 * b3 + a2 * b2 + a3 * b1 + local c4 = a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + local c5 = a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + local c6 = a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + local c7 = a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + + a7 * b1 + local c8, c9, c10, c11, c12, c13, c14 + if not half_multiply then + c8 = a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2 + c9 = a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3 + c10 = a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4 + c11 = a5 * b7 + a6 * b6 + a7 * b5 + c12 = a6 * b7 + a7 * b6 + c13 = a7 * b7 + c14 = 0 + else + c8 = 0 + end + + local temp + temp = c1 + c1 = c1 % 0x1000000 + c2 = c2 + (temp - c1) / 0x1000000 + temp = c2 + c2 = c2 % 0x1000000 + c3 = c3 + (temp - c2) / 0x1000000 + temp = c3 + c3 = c3 % 0x1000000 + c4 = c4 + (temp - c3) / 0x1000000 + temp = c4 + c4 = c4 % 0x1000000 + c5 = c5 + (temp - c4) / 0x1000000 + temp = c5 + c5 = c5 % 0x1000000 + c6 = c6 + (temp - c5) / 0x1000000 + temp = c6 + c6 = c6 % 0x1000000 + c7 = c7 + (temp - c6) / 0x1000000 + temp = c7 + c7 = c7 % 0x1000000 + if not half_multiply then + c8 = c8 + (temp - c7) / 0x1000000 + temp = c8 + c8 = c8 % 0x1000000 + c9 = c9 + (temp - c8) / 0x1000000 + temp = c9 + c9 = c9 % 0x1000000 + c10 = c10 + (temp - c9) / 0x1000000 + temp = c10 + c10 = c10 % 0x1000000 + c11 = c11 + (temp - c10) / 0x1000000 + temp = c11 + c11 = c11 % 0x1000000 + c12 = c12 + (temp - c11) / 0x1000000 + temp = c12 + c12 = c12 % 0x1000000 + c13 = c13 + (temp - c12) / 0x1000000 + temp = c13 + c13 = c13 % 0x1000000 + c14 = c14 + (temp - c13) / 0x1000000 + end + + return {c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14} + end + + local function square(a) + -- returns a 336-bit integer (14 words) + local a1, a2, a3, a4, a5, a6, a7 = unpack(a) + + local c1 = a1 * a1 + local c2 = a1 * a2 * 2 + local c3 = a1 * a3 * 2 + a2 * a2 + local c4 = a1 * a4 * 2 + a2 * a3 * 2 + local c5 = a1 * a5 * 2 + a2 * a4 * 2 + a3 * a3 + local c6 = a1 * a6 * 2 + a2 * a5 * 2 + a3 * a4 * 2 + local c7 = a1 * a7 * 2 + a2 * a6 * 2 + a3 * a5 * 2 + a4 * a4 + local c8 = a2 * a7 * 2 + a3 * a6 * 2 + a4 * a5 * 2 + local c9 = a3 * a7 * 2 + a4 * a6 * 2 + a5 * a5 + local c10 = a4 * a7 * 2 + a5 * a6 * 2 + local c11 = a5 * a7 * 2 + a6 * a6 + local c12 = a6 * a7 * 2 + local c13 = a7 * a7 + local c14 = 0 + + local temp + temp = c1 + c1 = c1 % 0x1000000 + c2 = c2 + (temp - c1) / 0x1000000 + temp = c2 + c2 = c2 % 0x1000000 + c3 = c3 + (temp - c2) / 0x1000000 + temp = c3 + c3 = c3 % 0x1000000 + c4 = c4 + (temp - c3) / 0x1000000 + temp = c4 + c4 = c4 % 0x1000000 + c5 = c5 + (temp - c4) / 0x1000000 + temp = c5 + c5 = c5 % 0x1000000 + c6 = c6 + (temp - c5) / 0x1000000 + temp = c6 + c6 = c6 % 0x1000000 + c7 = c7 + (temp - c6) / 0x1000000 + temp = c7 + c7 = c7 % 0x1000000 + c8 = c8 + (temp - c7) / 0x1000000 + temp = c8 + c8 = c8 % 0x1000000 + c9 = c9 + (temp - c8) / 0x1000000 + temp = c9 + c9 = c9 % 0x1000000 + c10 = c10 + (temp - c9) / 0x1000000 + temp = c10 + c10 = c10 % 0x1000000 + c11 = c11 + (temp - c10) / 0x1000000 + temp = c11 + c11 = c11 % 0x1000000 + c12 = c12 + (temp - c11) / 0x1000000 + temp = c12 + c12 = c12 % 0x1000000 + c13 = c13 + (temp - c12) / 0x1000000 + temp = c13 + c13 = c13 % 0x1000000 + c14 = c14 + (temp - c13) / 0x1000000 + + return {c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14} + end + + local function encodeInt(a) + local enc = {} + + for i = 1, 7 do + local word = a[i] + for j = 1, 3 do + enc[#enc + 1] = word % 256 + word = math.floor(word / 256) + end + end + + return enc + end + + local function decodeInt(enc) + local a = {} + local encCopy = {} + + for i = 1, 21 do + local byte = enc[i] + assert(type(byte) == "number", "integer decoding failure") + assert(byte >= 0 and byte <= 255, "integer decoding failure") + assert(byte % 1 == 0, "integer decoding failure") + encCopy[i] = byte + end + + for i = 1, 21, 3 do + local word = 0 + for j = 2, 0, -1 do + word = word * 256 + word = word + encCopy[i + j] + end + a[#a + 1] = word + end + + return a + end + + local function mods(d, w) + local result = d[1] % 2^w + + if result >= 2^(w - 1) then + result = result - 2^w + end + + return result + end + + -- Represents a 168-bit number as the (2^w)-ary Non-Adjacent Form + local function NAF(d, w) + local t = {} + local d = {unpack(d)} + + for i = 1, 168 do + if d[1] % 2 == 1 then + t[#t + 1] = mods(d, w) + d = sub(d, {t[#t], 0, 0, 0, 0, 0, 0}) + else + t[#t + 1] = 0 + end + + d = rShift(d) + end + + return t + end + + return { + isEqual = isEqual, + compare = compare, + add = add, + sub = sub, + addDouble = addDouble, + mult = mult, + square = square, + encodeInt = encodeInt, + decodeInt = decodeInt, + NAF = NAF + } +end)() + +-- Arithmetic on the finite field of integers modulo p +-- Where p is the finite field modulus +local modp = (function() + local add = arith.add + local sub = arith.sub + local addDouble = arith.addDouble + local mult = arith.mult + local square = arith.square + + local p = {3, 0, 0, 0, 0, 0, 15761408} + + -- We're using the Montgomery Reduction for fast modular multiplication. + -- https://en.wikipedia.org/wiki/Montgomery_modular_multiplication + -- r = 2^168 + -- p * pInverse = -1 (mod r) + -- r2 = r * r (mod p) + local pInverse = {5592405, 5592405, 5592405, 5592405, 5592405, 5592405, 14800213} + local r2 = {13533400, 837116, 6278376, 13533388, 837116, 6278376, 7504076} + + local function multByP(a) + local a1, a2, a3, a4, a5, a6, a7 = unpack(a) + + local c1 = a1 * 3 + local c2 = a2 * 3 + local c3 = a3 * 3 + local c4 = a4 * 3 + local c5 = a5 * 3 + local c6 = a6 * 3 + local c7 = a1 * 15761408 + c7 = c7 + a7 * 3 + local c8 = a2 * 15761408 + local c9 = a3 * 15761408 + local c10 = a4 * 15761408 + local c11 = a5 * 15761408 + local c12 = a6 * 15761408 + local c13 = a7 * 15761408 + local c14 = 0 + + local temp + temp = c1 / 0x1000000 + c2 = c2 + (temp - temp % 1) + c1 = c1 % 0x1000000 + temp = c2 / 0x1000000 + c3 = c3 + (temp - temp % 1) + c2 = c2 % 0x1000000 + temp = c3 / 0x1000000 + c4 = c4 + (temp - temp % 1) + c3 = c3 % 0x1000000 + temp = c4 / 0x1000000 + c5 = c5 + (temp - temp % 1) + c4 = c4 % 0x1000000 + temp = c5 / 0x1000000 + c6 = c6 + (temp - temp % 1) + c5 = c5 % 0x1000000 + temp = c6 / 0x1000000 + c7 = c7 + (temp - temp % 1) + c6 = c6 % 0x1000000 + temp = c7 / 0x1000000 + c8 = c8 + (temp - temp % 1) + c7 = c7 % 0x1000000 + temp = c8 / 0x1000000 + c9 = c9 + (temp - temp % 1) + c8 = c8 % 0x1000000 + temp = c9 / 0x1000000 + c10 = c10 + (temp - temp % 1) + c9 = c9 % 0x1000000 + temp = c10 / 0x1000000 + c11 = c11 + (temp - temp % 1) + c10 = c10 % 0x1000000 + temp = c11 / 0x1000000 + c12 = c12 + (temp - temp % 1) + c11 = c11 % 0x1000000 + temp = c12 / 0x1000000 + c13 = c13 + (temp - temp % 1) + c12 = c12 % 0x1000000 + temp = c13 / 0x1000000 + c14 = c14 + (temp - temp % 1) + c13 = c13 % 0x1000000 + + return {c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14} + end + + -- Reduces a number from [0, 2p - 1] to [0, p - 1] + local function reduceModP(a) + -- a < p + if a[7] < 15761408 or a[7] == 15761408 and a[1] < 3 then + return {unpack(a)} + end + + -- a > p + local c1 = a[1] + local c2 = a[2] + local c3 = a[3] + local c4 = a[4] + local c5 = a[5] + local c6 = a[6] + local c7 = a[7] + + c1 = c1 - 3 + c7 = c7 - 15761408 + + if c1 < 0 then + c2 = c2 - 1 + c1 = c1 + 0x1000000 + end + if c2 < 0 then + c3 = c3 - 1 + c2 = c2 + 0x1000000 + end + if c3 < 0 then + c4 = c4 - 1 + c3 = c3 + 0x1000000 + end + if c4 < 0 then + c5 = c5 - 1 + c4 = c4 + 0x1000000 + end + if c5 < 0 then + c6 = c6 - 1 + c5 = c5 + 0x1000000 + end + if c6 < 0 then + c7 = c7 - 1 + c6 = c6 + 0x1000000 + end + + return {c1, c2, c3, c4, c5, c6, c7} + end + + local function addModP(a, b) + return reduceModP(add(a, b)) + end + + local function subModP(a, b) + local result = sub(a, b) + + if result[7] < 0 then + result = add(result, p) + end + + return result + end + + -- Montgomery REDC algorithn + -- Reduces a number from [0, p^2 - 1] to [0, p - 1] + local function REDC(T) + local m = mult(T, pInverse, true) + local t = {unpack(addDouble(T, multByP(m)), 8, 14)} + + return reduceModP(t) + end + + local function multModP(a, b) + -- Only works with a, b in Montgomery form + return REDC(mult(a, b)) + end + + local function squareModP(a) + -- Only works with a in Montgomery form + return REDC(square(a)) + end + + local function montgomeryModP(a) + return multModP(a, r2) + end + + local function inverseMontgomeryModP(a) + local a = {unpack(a)} + + for i = 8, 14 do + a[i] = 0 + end + + return REDC(a) + end + + local ONE = montgomeryModP({1, 0, 0, 0, 0, 0, 0}) + + local function expModP(base, exponentBinary) + local base = {unpack(base)} + local result = {unpack(ONE)} + + for i = 1, 168 do + if exponentBinary[i] == 1 then + result = multModP(result, base) + end + base = squareModP(base) + end + + return result + end + + return { + addModP = addModP, + subModP = subModP, + multModP = multModP, + squareModP = squareModP, + montgomeryModP = montgomeryModP, + inverseMontgomeryModP = inverseMontgomeryModP, + expModP = expModP + } +end)() + +-- Arithmetic on the Finite Field of Integers modulo q +-- Where q is the generator's subgroup order. +local modq = (function() + local isEqual = arith.isEqual + local compare = arith.compare + local add = arith.add + local sub = arith.sub + local addDouble = arith.addDouble + local mult = arith.mult + local square = arith.square + local encodeInt = arith.encodeInt + local decodeInt = arith.decodeInt + + local modQMT + + local q = {9622359, 6699217, 13940450, 16775734, 16777215, 16777215, 3940351} + local qMinusTwoBinary = {1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1} + + -- We're using the Montgomery Reduction for fast modular multiplication. + -- https://en.wikipedia.org/wiki/Montgomery_modular_multiplication + -- r = 2^168 + -- q * qInverse = -1 (mod r) + -- r2 = r * r (mod q) + local qInverse = {15218585, 5740955, 3271338, 9903997, 9067368, 7173545, 6988392} + local r2 = {1336213, 11071705, 9716828, 11083885, 9188643, 1494868, 3306114} + + -- Reduces a number from [0, 2q - 1] to [0, q - 1] + local function reduceModQ(a) + local result = {unpack(a)} + + if compare(result, q) >= 0 then + result = sub(result, q) + end + + return setmetatable(result, modQMT) + end + + local function addModQ(a, b) + return reduceModQ(add(a, b)) + end + + local function subModQ(a, b) + local result = sub(a, b) + + if result[7] < 0 then + result = add(result, q) + end + + return setmetatable(result, modQMT) + end + + -- Montgomery REDC algorithn + -- Reduces a number from [0, q^2 - 1] to [0, q - 1] + local function REDC(T) + local m = {unpack(mult({unpack(T, 1, 7)}, qInverse, true), 1, 7)} + local t = {unpack(addDouble(T, mult(m, q)), 8, 14)} + + return reduceModQ(t) + end + + local function multModQ(a, b) + -- Only works with a, b in Montgomery form + return REDC(mult(a, b)) + end + + local function squareModQ(a) + -- Only works with a in Montgomery form + return REDC(square(a)) + end + + local function montgomeryModQ(a) + return multModQ(a, r2) + end + + local function inverseMontgomeryModQ(a) + local a = {unpack(a)} + + for i = 8, 14 do + a[i] = 0 + end + + return REDC(a) + end + + local ONE = montgomeryModQ({1, 0, 0, 0, 0, 0, 0}) + + local function expModQ(base, exponentBinary) + local base = {unpack(base)} + local result = {unpack(ONE)} + + for i = 1, 168 do + if exponentBinary[i] == 1 then + result = multModQ(result, base) + end + base = squareModQ(base) + end + + return result + end + + local function intExpModQ(base, exponent) + local base = {unpack(base)} + local result = setmetatable({unpack(ONE)}, modQMT) + + if exponent < 0 then + base = expModQ(base, qMinusTwoBinary) + exponent = -exponent + end + + while exponent > 0 do + if exponent % 2 == 1 then + result = multModQ(result, base) + end + base = squareModQ(base) + exponent = exponent / 2 + exponent = exponent - exponent % 1 + end + + return result + end + + local function encodeModQ(a) + local result = encodeInt(a) + + return setmetatable(result, byteTableMT) + end + + local function decodeModQ(s) + s = type(s) == "table" and {unpack(s, 1, 21)} or {tostring(s):byte(1, 21)} + local result = decodeInt(s) + result[7] = result[7] % q[7] + + return setmetatable(result, modQMT) + end + + local function randomModQ() + while true do + local s = os.urandom(21) + local result = decodeInt(s) + if result[7] < q[7] then + return setmetatable(result, modQMT) + end + end + end + + local function hashModQ(data) + return decodeModQ(sha256(data)) + end + + modQMT = { + __index = { + encode = function(self) + return encodeModQ(self) + end + }, + + __tostring = function(self) + return self:encode():toHex() + end, + + __add = function(self, other) + if type(self) == "number" then + return other + self + end + + if type(other) == "number" then + assert(other < 2^24, "number operand too big") + other = montgomeryModQ({other, 0, 0, 0, 0, 0, 0}) + end + + return addModQ(self, other) + end, + + __sub = function(a, b) + if type(a) == "number" then + assert(a < 2^24, "number operand too big") + a = montgomeryModQ({a, 0, 0, 0, 0, 0, 0}) + end + + if type(b) == "number" then + assert(b < 2^24, "number operand too big") + b = montgomeryModQ({b, 0, 0, 0, 0, 0, 0}) + end + + return subModQ(a, b) + end, + + __unm = function(self) + return subModQ(q, self) + end, + + __eq = function(self, other) + return isEqual(self, other) + end, + + __mul = function(self, other) + if type(self) == "number" then + return other * self + end + + -- EC point + -- Use the point's metatable to handle multiplication + if type(other) == "table" and type(other[1]) == "table" then + return other * self + end + + if type(other) == "number" then + assert(other < 2^24, "number operand too big") + other = montgomeryModQ({other, 0, 0, 0, 0, 0, 0}) + end + + return multModQ(self, other) + end, + + __div = function(a, b) + if type(a) == "number" then + assert(a < 2^24, "number operand too big") + a = montgomeryModQ({a, 0, 0, 0, 0, 0, 0}) + end + + if type(b) == "number" then + assert(b < 2^24, "number operand too big") + b = montgomeryModQ({b, 0, 0, 0, 0, 0, 0}) + end + + local bInv = expModQ(b, qMinusTwoBinary) + + return multModQ(a, bInv) + end, + + __pow = function(self, other) + return intExpModQ(self, other) + end + } + + return { + hashModQ = hashModQ, + randomModQ = randomModQ, + decodeModQ = decodeModQ, + inverseMontgomeryModQ = inverseMontgomeryModQ + } +end)() + +-- Elliptic curve arithmetic +local curve = (function() + ---- About the Curve Itself + -- Field Size: 168 bits + -- Field Modulus (p): 481 * 2^159 + 3 + -- Equation: x^2 + y^2 = 1 + 122 * x^2 * y^2 + -- Parameters: Edwards Curve with d = 122 + -- Curve Order (n): 351491143778082151827986174289773107581916088585564 + -- Cofactor (h): 4 + -- Generator Order (q): 87872785944520537956996543572443276895479022146391 + ---- About the Curve's Security + -- Current best attack security: 81.777 bits (Small Subgroup + Rho) + -- Rho Security: log2(0.884 * sqrt(q)) = 82.777 bits + -- Transfer Security? Yes: p ~= q; k > 20 + -- Field Discriminant Security? Yes: + -- t = 27978492958645335688000168 + -- s = 10 + -- |D| = 6231685068753619775430107799412237267322159383147 > 2^100 + -- Rigidity? No, not at all. + -- XZ/YZ Ladder Security? No: Single coordinate ladders are insecure. + -- Small Subgroup Security? No. + -- Invalid Curve Security? Yes: Points are checked before every operation. + -- Invalid Curve Twist Security? No: Don't use single coordinate ladders. + -- Completeness? Yes: The curve is complete. + -- Indistinguishability? Yes (Elligator 2), but not implemented. + + local isEqual = arith.isEqual + local NAF = arith.NAF + local encodeInt = arith.encodeInt + local decodeInt = arith.decodeInt + local multModP = modp.multModP + local squareModP = modp.squareModP + local addModP = modp.addModP + local subModP = modp.subModP + local montgomeryModP = modp.montgomeryModP + local expModP = modp.expModP + local inverseMontgomeryModQ = modq.inverseMontgomeryModQ + + local pointMT + local ZERO = {0, 0, 0, 0, 0, 0, 0} + local ONE = montgomeryModP({1, 0, 0, 0, 0, 0, 0}) + + -- Curve Parameters + local d = montgomeryModP({122, 0, 0, 0, 0, 0, 0}) + local p = {3, 0, 0, 0, 0, 0, 15761408} + local pMinusTwoBinary = {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1} + local pMinusThreeOverFourBinary = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1} + local G = { + {6636044, 10381432, 15741790, 2914241, 5785600, 264923, 4550291}, + {13512827, 8449886, 5647959, 1135556, 5489843, 7177356, 8002203}, + {unpack(ONE)} + } + local O = { + {unpack(ZERO)}, + {unpack(ONE)}, + {unpack(ONE)} + } + + -- Projective Coordinates for Edwards curves for point addition/doubling. + -- Points are represented as: (X:Y:Z) where x = X/Z and y = Y/Z + -- The identity element is represented by (0:1:1) + -- Point operation formulas are available on the EFD: + -- https://www.hyperelliptic.org/EFD/g1p/auto-edwards-projective.html + local function pointDouble(P1) + -- 3M + 4S + local X1, Y1, Z1 = unpack(P1) + + local b = addModP(X1, Y1) + local B = squareModP(b) + local C = squareModP(X1) + local D = squareModP(Y1) + local E = addModP(C, D) + local H = squareModP(Z1) + local J = subModP(E, addModP(H, H)) + local X3 = multModP(subModP(B, E), J) + local Y3 = multModP(E, subModP(C, D)) + local Z3 = multModP(E, J) + local P3 = {X3, Y3, Z3} + + return setmetatable(P3, pointMT) + end + + local function pointAdd(P1, P2) + -- 10M + 1S + local X1, Y1, Z1 = unpack(P1) + local X2, Y2, Z2 = unpack(P2) + + local A = multModP(Z1, Z2) + local B = squareModP(A) + local C = multModP(X1, X2) + local D = multModP(Y1, Y2) + local E = multModP(d, multModP(C, D)) + local F = subModP(B, E) + local G = addModP(B, E) + local X3 = multModP(A, multModP(F, subModP(multModP(addModP(X1, Y1), addModP(X2, Y2)), addModP(C, D)))) + local Y3 = multModP(A, multModP(G, subModP(D, C))) + local Z3 = multModP(F, G) + local P3 = {X3, Y3, Z3} + + return setmetatable(P3, pointMT) + end + + local function pointNeg(P1) + local X1, Y1, Z1 = unpack(P1) + + local X3 = subModP(ZERO, X1) + local Y3 = {unpack(Y1)} + local Z3 = {unpack(Z1)} + local P3 = {X3, Y3, Z3} + + return setmetatable(P3, pointMT) + end + + local function pointSub(P1, P2) + return pointAdd(P1, pointNeg(P2)) + end + + -- Converts (X:Y:Z) into (X:Y:1) = (x:y:1) + local function pointScale(P1) + local X1, Y1, Z1 = unpack(P1) + + local A = expModP(Z1, pMinusTwoBinary) + local X3 = multModP(X1, A) + local Y3 = multModP(Y1, A) + local Z3 = {unpack(ONE)} + local P3 = {X3, Y3, Z3} + + return setmetatable(P3, pointMT) + end + + local function pointIsEqual(P1, P2) + local X1, Y1, Z1 = unpack(P1) + local X2, Y2, Z2 = unpack(P2) + + local A1 = multModP(X1, Z2) + local B1 = multModP(Y1, Z2) + local A2 = multModP(X2, Z1) + local B2 = multModP(Y2, Z1) + + return isEqual(A1, A2) and isEqual(B1, B2) + end + + -- Checks if a projective point satisfies the curve equation + local function pointIsOnCurve(P1) + local X1, Y1, Z1 = unpack(P1) + + local X12 = squareModP(X1) + local Y12 = squareModP(Y1) + local Z12 = squareModP(Z1) + local Z14 = squareModP(Z12) + local a = addModP(X12, Y12) + a = multModP(a, Z12) + local b = multModP(d, multModP(X12, Y12)) + b = addModP(Z14, b) + + return isEqual(a, b) + end + + local function pointIsInf(P1) + return isEqual(P1[1], ZERO) + end + + -- W-ary Non-Adjacent Form (wNAF) method for scalar multiplication: + -- https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#w-ary_non-adjacent_form_(wNAF)_method + local function scalarMult(multiplier, P1) + -- w = 5 + local naf = NAF(multiplier, 5) + local PTable = {P1} + local P2 = pointDouble(P1) + local Q = {{unpack(ZERO)}, {unpack(ONE)}, {unpack(ONE)}} + + for i = 3, 31, 2 do + PTable[i] = pointAdd(PTable[i - 2], P2) + end + + for i = #naf, 1, -1 do + Q = pointDouble(Q) + if naf[i] > 0 then + Q = pointAdd(Q, PTable[naf[i]]) + elseif naf[i] < 0 then + Q = pointSub(Q, PTable[-naf[i]]) + end + end + + return setmetatable(Q, pointMT) + end + + -- Lookup table 4-ary NAF method for scalar multiplication by G. + -- Precomputations for the regular NAF method are done before the multiplication. + local GTable = {G} + for i = 2, 168 do + GTable[i] = pointDouble(GTable[i - 1]) + end + + local function scalarMultG(multiplier) + local naf = NAF(multiplier, 2) + local Q = {{unpack(ZERO)}, {unpack(ONE)}, {unpack(ONE)}} + + for i = 1, 168 do + if naf[i] == 1 then + Q = pointAdd(Q, GTable[i]) + elseif naf[i] == -1 then + Q = pointSub(Q, GTable[i]) + end + end + + return setmetatable(Q, pointMT) + end + + -- Point compression and encoding. + -- Compresses curve points to 22 bytes. + local function pointEncode(P1) + P1 = pointScale(P1) + local result = {} + local x, y = unpack(P1) + + -- Encode y + result = encodeInt(y) + -- Encode one bit from x + result[22] = x[1] % 2 + + return setmetatable(result, byteTableMT) + end + + local function pointDecode(enc) + enc = type(enc) == "table" and {unpack(enc, 1, 22)} or {tostring(enc):byte(1, 22)} + -- Decode y + local y = decodeInt(enc) + y[7] = y[7] % p[7] + -- Find {x, -x} using curve equation + local y2 = squareModP(y) + local u = subModP(y2, ONE) + local v = subModP(multModP(d, y2), ONE) + local u2 = squareModP(u) + local u3 = multModP(u, u2) + local u5 = multModP(u3, u2) + local v3 = multModP(v, squareModP(v)) + local w = multModP(u5, v3) + local x = multModP(u3, multModP(v, expModP(w, pMinusThreeOverFourBinary))) + -- Use enc[22] to find x from {x, -x} + if x[1] % 2 ~= enc[22] then + x = subModP(ZERO, x) + end + local P3 = {x, y, {unpack(ONE)}} + + return setmetatable(P3, pointMT) + end + + pointMT = { + __index = { + isOnCurve = function(self) + return pointIsOnCurve(self) + end, + + isInf = function(self) + return self:isOnCurve() and pointIsInf(self) + end, + + encode = function(self) + return pointEncode(self) + end + }, + + __tostring = function(self) + return self:encode():toHex() + end, + + __add = function(P1, P2) + assert(P1:isOnCurve(), "invalid point") + assert(P2:isOnCurve(), "invalid point") + + return pointAdd(P1, P2) + end, + + __sub = function(P1, P2) + assert(P1:isOnCurve(), "invalid point") + assert(P2:isOnCurve(), "invalid point") + + return pointSub(P1, P2) + end, + + __unm = function(self) + assert(self:isOnCurve(), "invalid point") + + return pointNeg(self) + end, + + __eq = function(P1, P2) + assert(P1:isOnCurve(), "invalid point") + assert(P2:isOnCurve(), "invalid point") + + return pointIsEqual(P1, P2) + end, + + __mul = function(P1, s) + if type(P1) == "number" then + return s * P1 + end + + if type(s) == "number" then + assert(s < 2^24, "number multiplier too big") + s = {s, 0, 0, 0, 0, 0, 0} + else + s = inverseMontgomeryModQ(s) + end + + if P1 == G then + return scalarMultG(s) + else + return scalarMult(s, P1) + end + end + } + + G = setmetatable(G, pointMT) + O = setmetatable(O, pointMT) + + return { + G = G, + O = O, + pointDecode = pointDecode + } +end)() + +local function getNonceFromEpoch() + local nonce = {} + local epoch = os.epoch("utc") + for i = 1, 12 do + nonce[#nonce + 1] = epoch % 256 + epoch = epoch / 256 + epoch = epoch - epoch % 1 + end + + return nonce +end + +local function keypair(seed) + local x + if seed then + x = modq.hashModQ(seed) + else + x = modq.randomModQ() + end + local Y = curve.G * x + + local privateKey = x:encode() + local publicKey = Y:encode() + + return privateKey, publicKey +end + +local function exchange(privateKey, publicKey) + local x = modq.decodeModQ(privateKey) + local Y = curve.pointDecode(publicKey) + + local Z = Y * x + + local sharedSecret = sha256(Z:encode()) + + return sharedSecret +end + +local function sign(privateKey, message) + local message = type(message) == "table" and string.char(unpack(message)) or tostring(message) + local privateKey = type(privateKey) == "table" and string.char(unpack(privateKey)) or tostring(privateKey) + local x = modq.decodeModQ(privateKey) + local k = modq.randomModQ() + local R = curve.G * k + local e = modq.hashModQ(message .. tostring(R)) + local s = k - x * e + + e = e:encode() + s = s:encode() + + local result = e + for i = 1, #s do + result[#result + 1] = s[i] + end + + return setmetatable(result, byteTableMT) +end + +local function verify(publicKey, message, signature) + local message = type(message) == "table" and string.char(unpack(message)) or tostring(message) + local Y = curve.pointDecode(publicKey) + local e = modq.decodeModQ({unpack(signature, 1, #signature / 2)}) + local s = modq.decodeModQ({unpack(signature, #signature / 2 + 1)}) + local Rv = curve.G * s + Y * e + local ev = modq.hashModQ(message .. tostring(Rv)) + + return ev == e +end + +return { + keypair = keypair, + exchange = exchange, + sign = sign, + verify = verify +} \ No newline at end of file diff --git a/src/lib/ecc.lua b/src/lib/ecc.lua new file mode 100644 index 0000000..b65b3f7 --- /dev/null +++ b/src/lib/ecc.lua @@ -0,0 +1,2135 @@ +-- Elliptic curve cryptography library. This should probably be minified? Why does it have its own `irequire`? + +local preload = _G.package.preload + +local irequire = _G.require +if type(irequire) ~= "function" then + local loading = {} + local loaded = {} + irequire = function(name) + local result = loaded[name] + + if result ~= nil then + if result == loading then + error("loop or previous error loading module '" .. name .. "'", 2) + end + + return result + end + + loaded[name] = loading + local contents = preload[name] + if contents then + result = contents(name) + else + error("cannot load '" .. name .. "'", 2) + end + + if result == nil then result = true end + loaded[name] = result + return result + end +end +preload["fq"] = function(...) +-- Fq Integer Arithmetic + +local bxor = bit32.bxor or bit.bxor +local n = 0xffff +local m = 0x10000 + +local q = {1372, 62520, 47765, 8105, 45059, 9616, 65535, 65535, 65535, 65535, 65535, 65532} +local qn = {1372, 62520, 47765, 8105, 45059, 9616, 65535, 65535, 65535, 65535, 65535, 65532, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + +local mt = { + __tostring = function(a) return string.char(unpack(a)) end, + __index = { + toHex = function(self, s) return ("%02x"):rep(#self):format(unpack(self)) end, + isEqual = function(self, t) + if type(t) ~= "table" then return false end + if #self ~= #t then return false end + local ret = 0 + for i = 1, #self do + ret = bit32.bor(ret, bxor(self[i], t[i])) + end + return ret == 0 + end + } +} + +local function eq(a, b) + for i = 1, 12 do + if a[i] ~= b[i] then + return false + end + end + + return true +end + +local function cmp(a, b) + for i = 12, 1, -1 do + if a[i] > b[i] then + return 1 + elseif a[i] < b[i] then + return -1 + end + end + + return 0 +end + +local function cmp384(a, b) + for i = 24, 1, -1 do + if a[i] > b[i] then + return 1 + elseif a[i] < b[i] then + return -1 + end + end + + return 0 +end + +local function bytes(x) + local result = {} + + for i = 0, 11 do + local m = x[i + 1] % 256 + result[2 * i + 1] = m + result[2 * i + 2] = (x[i + 1] - m) / 256 + end + + return setmetatable(result, mt) +end + +local function fromBytes(enc) + local result = {} + + for i = 0, 11 do + result[i + 1] = enc[2 * i + 1] % 256 + result[i + 1] = result[i + 1] + enc[2 * i + 2] * 256 + end + + return result +end + +local function sub192(a, b) + local r1 = a[1] - b[1] + local r2 = a[2] - b[2] + local r3 = a[3] - b[3] + local r4 = a[4] - b[4] + local r5 = a[5] - b[5] + local r6 = a[6] - b[6] + local r7 = a[7] - b[7] + local r8 = a[8] - b[8] + local r9 = a[9] - b[9] + local r10 = a[10] - b[10] + local r11 = a[11] - b[11] + local r12 = a[12] - b[12] + + if r1 < 0 then + r2 = r2 - 1 + r1 = r1 + m + end + if r2 < 0 then + r3 = r3 - 1 + r2 = r2 + m + end + if r3 < 0 then + r4 = r4 - 1 + r3 = r3 + m + end + if r4 < 0 then + r5 = r5 - 1 + r4 = r4 + m + end + if r5 < 0 then + r6 = r6 - 1 + r5 = r5 + m + end + if r6 < 0 then + r7 = r7 - 1 + r6 = r6 + m + end + if r7 < 0 then + r8 = r8 - 1 + r7 = r7 + m + end + if r8 < 0 then + r9 = r9 - 1 + r8 = r8 + m + end + if r9 < 0 then + r10 = r10 - 1 + r9 = r9 + m + end + if r10 < 0 then + r11 = r11 - 1 + r10 = r10 + m + end + if r11 < 0 then + r12 = r12 - 1 + r11 = r11 + m + end + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12} + + return result +end + +local function reduce(a) + local result = {unpack(a)} + + if cmp(result, q) >= 0 then + result = sub192(result, q) + end + + return result +end + +local function add(a, b) + local r1 = a[1] + b[1] + local r2 = a[2] + b[2] + local r3 = a[3] + b[3] + local r4 = a[4] + b[4] + local r5 = a[5] + b[5] + local r6 = a[6] + b[6] + local r7 = a[7] + b[7] + local r8 = a[8] + b[8] + local r9 = a[9] + b[9] + local r10 = a[10] + b[10] + local r11 = a[11] + b[11] + local r12 = a[12] + b[12] + + if r1 > n then + r2 = r2 + 1 + r1 = r1 - m + end + if r2 > n then + r3 = r3 + 1 + r2 = r2 - m + end + if r3 > n then + r4 = r4 + 1 + r3 = r3 - m + end + if r4 > n then + r5 = r5 + 1 + r4 = r4 - m + end + if r5 > n then + r6 = r6 + 1 + r5 = r5 - m + end + if r6 > n then + r7 = r7 + 1 + r6 = r6 - m + end + if r7 > n then + r8 = r8 + 1 + r7 = r7 - m + end + if r8 > n then + r9 = r9 + 1 + r8 = r8 - m + end + if r9 > n then + r10 = r10 + 1 + r9 = r9 - m + end + if r10 > n then + r11 = r11 + 1 + r10 = r10 - m + end + if r11 > n then + r12 = r12 + 1 + r11 = r11 - m + end + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12} + + return reduce(result) +end + +local function sub(a, b) + local result = sub192(a, b) + + if result[12] < 0 then + result = add(result, q) + end + + return result +end + +local function add384(a, b) + local r1 = a[1] + b[1] + local r2 = a[2] + b[2] + local r3 = a[3] + b[3] + local r4 = a[4] + b[4] + local r5 = a[5] + b[5] + local r6 = a[6] + b[6] + local r7 = a[7] + b[7] + local r8 = a[8] + b[8] + local r9 = a[9] + b[9] + local r10 = a[10] + b[10] + local r11 = a[11] + b[11] + local r12 = a[12] + b[12] + local r13 = a[13] + b[13] + local r14 = a[14] + b[14] + local r15 = a[15] + b[15] + local r16 = a[16] + b[16] + local r17 = a[17] + b[17] + local r18 = a[18] + b[18] + local r19 = a[19] + b[19] + local r20 = a[20] + b[20] + local r21 = a[21] + b[21] + local r22 = a[22] + b[22] + local r23 = a[23] + b[23] + local r24 = a[24] + b[24] + + if r1 > n then + r2 = r2 + 1 + r1 = r1 - m + end + if r2 > n then + r3 = r3 + 1 + r2 = r2 - m + end + if r3 > n then + r4 = r4 + 1 + r3 = r3 - m + end + if r4 > n then + r5 = r5 + 1 + r4 = r4 - m + end + if r5 > n then + r6 = r6 + 1 + r5 = r5 - m + end + if r6 > n then + r7 = r7 + 1 + r6 = r6 - m + end + if r7 > n then + r8 = r8 + 1 + r7 = r7 - m + end + if r8 > n then + r9 = r9 + 1 + r8 = r8 - m + end + if r9 > n then + r10 = r10 + 1 + r9 = r9 - m + end + if r10 > n then + r11 = r11 + 1 + r10 = r10 - m + end + if r11 > n then + r12 = r12 + 1 + r11 = r11 - m + end + if r12 > n then + r13 = r13 + 1 + r12 = r12 - m + end + if r13 > n then + r14 = r14 + 1 + r13 = r13 - m + end + if r14 > n then + r15 = r15 + 1 + r14 = r14 - m + end + if r15 > n then + r16 = r16 + 1 + r15 = r15 - m + end + if r16 > n then + r17 = r17 + 1 + r16 = r16 - m + end + if r17 > n then + r18 = r18 + 1 + r17 = r17 - m + end + if r18 > n then + r19 = r19 + 1 + r18 = r18 - m + end + if r19 > n then + r20 = r20 + 1 + r19 = r19 - m + end + if r20 > n then + r21 = r21 + 1 + r20 = r20 - m + end + if r21 > n then + r22 = r22 + 1 + r21 = r21 - m + end + if r22 > n then + r23 = r23 + 1 + r22 = r22 - m + end + if r23 > n then + r24 = r24 + 1 + r23 = r23 - m + end + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24} + + return result +end + +local function sub384(a, b) + local r1 = a[1] - b[1] + local r2 = a[2] - b[2] + local r3 = a[3] - b[3] + local r4 = a[4] - b[4] + local r5 = a[5] - b[5] + local r6 = a[6] - b[6] + local r7 = a[7] - b[7] + local r8 = a[8] - b[8] + local r9 = a[9] - b[9] + local r10 = a[10] - b[10] + local r11 = a[11] - b[11] + local r12 = a[12] - b[12] + local r13 = a[13] - b[13] + local r14 = a[14] - b[14] + local r15 = a[15] - b[15] + local r16 = a[16] - b[16] + local r17 = a[17] - b[17] + local r18 = a[18] - b[18] + local r19 = a[19] - b[19] + local r20 = a[20] - b[20] + local r21 = a[21] - b[21] + local r22 = a[22] - b[22] + local r23 = a[23] - b[23] + local r24 = a[24] - b[24] + + if r1 < 0 then + r2 = r2 - 1 + r1 = r1 + m + end + if r2 < 0 then + r3 = r3 - 1 + r2 = r2 + m + end + if r3 < 0 then + r4 = r4 - 1 + r3 = r3 + m + end + if r4 < 0 then + r5 = r5 - 1 + r4 = r4 + m + end + if r5 < 0 then + r6 = r6 - 1 + r5 = r5 + m + end + if r6 < 0 then + r7 = r7 - 1 + r6 = r6 + m + end + if r7 < 0 then + r8 = r8 - 1 + r7 = r7 + m + end + if r8 < 0 then + r9 = r9 - 1 + r8 = r8 + m + end + if r9 < 0 then + r10 = r10 - 1 + r9 = r9 + m + end + if r10 < 0 then + r11 = r11 - 1 + r10 = r10 + m + end + if r11 < 0 then + r12 = r12 - 1 + r11 = r11 + m + end + if r12 < 0 then + r13 = r13 - 1 + r12 = r12 + m + end + if r13 < 0 then + r14 = r14 - 1 + r13 = r13 + m + end + if r14 < 0 then + r15 = r15 - 1 + r14 = r14 + m + end + if r15 < 0 then + r16 = r16 - 1 + r15 = r15 + m + end + if r16 < 0 then + r17 = r17 - 1 + r16 = r16 + m + end + if r17 < 0 then + r18 = r18 - 1 + r17 = r17 + m + end + if r18 < 0 then + r19 = r19 - 1 + r18 = r18 + m + end + if r19 < 0 then + r20 = r20 - 1 + r19 = r19 + m + end + if r20 < 0 then + r21 = r21 - 1 + r20 = r20 + m + end + if r21 < 0 then + r22 = r22 - 1 + r21 = r21 + m + end + if r22 < 0 then + r23 = r23 - 1 + r22 = r22 + m + end + if r23 < 0 then + r24 = r24 - 1 + r23 = r23 + m + end + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24} + + return result +end + +local function mul384(a, b) + local a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 = unpack(a) + local b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12 = unpack(b) + + local r1 = a1 * b1 + + local r2 = a1 * b2 + r2 = r2 + a2 * b1 + + local r3 = a1 * b3 + r3 = r3 + a2 * b2 + r3 = r3 + a3 * b1 + + local r4 = a1 * b4 + r4 = r4 + a2 * b3 + r4 = r4 + a3 * b2 + r4 = r4 + a4 * b1 + + local r5 = a1 * b5 + r5 = r5 + a2 * b4 + r5 = r5 + a3 * b3 + r5 = r5 + a4 * b2 + r5 = r5 + a5 * b1 + + local r6 = a1 * b6 + r6 = r6 + a2 * b5 + r6 = r6 + a3 * b4 + r6 = r6 + a4 * b3 + r6 = r6 + a5 * b2 + r6 = r6 + a6 * b1 + + local r7 = a1 * b7 + r7 = r7 + a2 * b6 + r7 = r7 + a3 * b5 + r7 = r7 + a4 * b4 + r7 = r7 + a5 * b3 + r7 = r7 + a6 * b2 + r7 = r7 + a7 * b1 + + local r8 = a1 * b8 + r8 = r8 + a2 * b7 + r8 = r8 + a3 * b6 + r8 = r8 + a4 * b5 + r8 = r8 + a5 * b4 + r8 = r8 + a6 * b3 + r8 = r8 + a7 * b2 + r8 = r8 + a8 * b1 + + local r9 = a1 * b9 + r9 = r9 + a2 * b8 + r9 = r9 + a3 * b7 + r9 = r9 + a4 * b6 + r9 = r9 + a5 * b5 + r9 = r9 + a6 * b4 + r9 = r9 + a7 * b3 + r9 = r9 + a8 * b2 + r9 = r9 + a9 * b1 + + local r10 = a1 * b10 + r10 = r10 + a2 * b9 + r10 = r10 + a3 * b8 + r10 = r10 + a4 * b7 + r10 = r10 + a5 * b6 + r10 = r10 + a6 * b5 + r10 = r10 + a7 * b4 + r10 = r10 + a8 * b3 + r10 = r10 + a9 * b2 + r10 = r10 + a10 * b1 + + local r11 = a1 * b11 + r11 = r11 + a2 * b10 + r11 = r11 + a3 * b9 + r11 = r11 + a4 * b8 + r11 = r11 + a5 * b7 + r11 = r11 + a6 * b6 + r11 = r11 + a7 * b5 + r11 = r11 + a8 * b4 + r11 = r11 + a9 * b3 + r11 = r11 + a10 * b2 + r11 = r11 + a11 * b1 + + local r12 = a1 * b12 + r12 = r12 + a2 * b11 + r12 = r12 + a3 * b10 + r12 = r12 + a4 * b9 + r12 = r12 + a5 * b8 + r12 = r12 + a6 * b7 + r12 = r12 + a7 * b6 + r12 = r12 + a8 * b5 + r12 = r12 + a9 * b4 + r12 = r12 + a10 * b3 + r12 = r12 + a11 * b2 + r12 = r12 + a12 * b1 + + local r13 = a2 * b12 + r13 = r13 + a3 * b11 + r13 = r13 + a4 * b10 + r13 = r13 + a5 * b9 + r13 = r13 + a6 * b8 + r13 = r13 + a7 * b7 + r13 = r13 + a8 * b6 + r13 = r13 + a9 * b5 + r13 = r13 + a10 * b4 + r13 = r13 + a11 * b3 + r13 = r13 + a12 * b2 + + local r14 = a3 * b12 + r14 = r14 + a4 * b11 + r14 = r14 + a5 * b10 + r14 = r14 + a6 * b9 + r14 = r14 + a7 * b8 + r14 = r14 + a8 * b7 + r14 = r14 + a9 * b6 + r14 = r14 + a10 * b5 + r14 = r14 + a11 * b4 + r14 = r14 + a12 * b3 + + local r15 = a4 * b12 + r15 = r15 + a5 * b11 + r15 = r15 + a6 * b10 + r15 = r15 + a7 * b9 + r15 = r15 + a8 * b8 + r15 = r15 + a9 * b7 + r15 = r15 + a10 * b6 + r15 = r15 + a11 * b5 + r15 = r15 + a12 * b4 + + local r16 = a5 * b12 + r16 = r16 + a6 * b11 + r16 = r16 + a7 * b10 + r16 = r16 + a8 * b9 + r16 = r16 + a9 * b8 + r16 = r16 + a10 * b7 + r16 = r16 + a11 * b6 + r16 = r16 + a12 * b5 + + local r17 = a6 * b12 + r17 = r17 + a7 * b11 + r17 = r17 + a8 * b10 + r17 = r17 + a9 * b9 + r17 = r17 + a10 * b8 + r17 = r17 + a11 * b7 + r17 = r17 + a12 * b6 + + local r18 = a7 * b12 + r18 = r18 + a8 * b11 + r18 = r18 + a9 * b10 + r18 = r18 + a10 * b9 + r18 = r18 + a11 * b8 + r18 = r18 + a12 * b7 + + local r19 = a8 * b12 + r19 = r19 + a9 * b11 + r19 = r19 + a10 * b10 + r19 = r19 + a11 * b9 + r19 = r19 + a12 * b8 + + local r20 = a9 * b12 + r20 = r20 + a10 * b11 + r20 = r20 + a11 * b10 + r20 = r20 + a12 * b9 + + local r21 = a10 * b12 + r21 = r21 + a11 * b11 + r21 = r21 + a12 * b10 + + local r22 = a11 * b12 + r22 = r22 + a12 * b11 + + local r23 = a12 * b12 + + local r24 = 0 + + r2 = r2 + (r1 / m) + r2 = r2 - r2 % 1 + r1 = r1 % m + r3 = r3 + (r2 / m) + r3 = r3 - r3 % 1 + r2 = r2 % m + r4 = r4 + (r3 / m) + r4 = r4 - r4 % 1 + r3 = r3 % m + r5 = r5 + (r4 / m) + r5 = r5 - r5 % 1 + r4 = r4 % m + r6 = r6 + (r5 / m) + r6 = r6 - r6 % 1 + r5 = r5 % m + r7 = r7 + (r6 / m) + r7 = r7 - r7 % 1 + r6 = r6 % m + r8 = r8 + (r7 / m) + r8 = r8 - r8 % 1 + r7 = r7 % m + r9 = r9 + (r8 / m) + r9 = r9 - r9 % 1 + r8 = r8 % m + r10 = r10 + (r9 / m) + r10 = r10 - r10 % 1 + r9 = r9 % m + r11 = r11 + (r10 / m) + r11 = r11 - r11 % 1 + r10 = r10 % m + r12 = r12 + (r11 / m) + r12 = r12 - r12 % 1 + r11 = r11 % m + r13 = r13 + (r12 / m) + r13 = r13 - r13 % 1 + r12 = r12 % m + r14 = r14 + (r13 / m) + r14 = r14 - r14 % 1 + r13 = r13 % m + r15 = r15 + (r14 / m) + r15 = r15 - r15 % 1 + r14 = r14 % m + r16 = r16 + (r15 / m) + r16 = r16 - r16 % 1 + r15 = r15 % m + r17 = r17 + (r16 / m) + r17 = r17 - r17 % 1 + r16 = r16 % m + r18 = r18 + (r17 / m) + r18 = r18 - r18 % 1 + r17 = r17 % m + r19 = r19 + (r18 / m) + r19 = r19 - r19 % 1 + r18 = r18 % m + r20 = r20 + (r19 / m) + r20 = r20 - r20 % 1 + r19 = r19 % m + r21 = r21 + (r20 / m) + r21 = r21 - r21 % 1 + r20 = r20 % m + r22 = r22 + (r21 / m) + r22 = r22 - r22 % 1 + r21 = r21 % m + r23 = r23 + (r22 / m) + r23 = r23 - r23 % 1 + r22 = r22 % m + r24 = r24 + (r23 / m) + r24 = r24 - r24 % 1 + r23 = r23 % m + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24} + + return result +end + +local function reduce384(a) + local result = {unpack(a)} + + while cmp384(result, qn) >= 0 do + local qn = {unpack(qn)} + local qn2 = add384(qn, qn) + while cmp384(result, qn2) > 0 do + qn = qn2 + qn2 = add384(qn2, qn2) + end + result = sub384(result, qn) + end + + result = {unpack(result, 1, 12)} + + return result +end + +local function mul(a, b) + return reduce384(mul384(a, b)) +end + +return { + fromBytes = fromBytes, + bytes = bytes, + sub = sub, + mul = mul, + eq = eq, + cmp = cmp, +} +end +preload["fp"] = function(...) +-- Fp Integer Arithmetic + +local n = 0xffff +local m = 0x10000 + +local p = {3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65533} +local p2 = {21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 43690} +local r2 = {44014, 58358, 19452, 6484, 45852, 58974, 63348, 64806, 65292, 65454, 65508, 21512} + +local function eq(a, b) + for i = 1, 12 do + if a[i] ~= b[i] then + return false + end + end + + return true +end + +local function reduce(a) + local r1 = a[1] + local r2 = a[2] + local r3 = a[3] + local r4 = a[4] + local r5 = a[5] + local r6 = a[6] + local r7 = a[7] + local r8 = a[8] + local r9 = a[9] + local r10 = a[10] + local r11 = a[11] + local r12 = a[12] + + if r12 < 65533 or r12 == 65533 and r1 < 3 then + return {unpack(a)} + end + + r1 = r1 - 3 + r12 = r12 - 65533 + + if r1 < 0 then + r2 = r2 - 1 + r1 = r1 + m + end + if r2 < 0 then + r3 = r3 - 1 + r2 = r2 + m + end + if r3 < 0 then + r4 = r4 - 1 + r3 = r3 + m + end + if r4 < 0 then + r5 = r5 - 1 + r4 = r4 + m + end + if r5 < 0 then + r6 = r6 - 1 + r5 = r5 + m + end + if r6 < 0 then + r7 = r7 - 1 + r6 = r6 + m + end + if r7 < 0 then + r8 = r8 - 1 + r7 = r7 + m + end + if r8 < 0 then + r9 = r9 - 1 + r8 = r8 + m + end + if r9 < 0 then + r10 = r10 - 1 + r9 = r9 + m + end + if r10 < 0 then + r11 = r11 - 1 + r10 = r10 + m + end + if r11 < 0 then + r12 = r12 - 1 + r11 = r11 + m + end + + return {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12} +end + +local function add(a, b) + local r1 = a[1] + b[1] + local r2 = a[2] + b[2] + local r3 = a[3] + b[3] + local r4 = a[4] + b[4] + local r5 = a[5] + b[5] + local r6 = a[6] + b[6] + local r7 = a[7] + b[7] + local r8 = a[8] + b[8] + local r9 = a[9] + b[9] + local r10 = a[10] + b[10] + local r11 = a[11] + b[11] + local r12 = a[12] + b[12] + + if r1 > n then + r2 = r2 + 1 + r1 = r1 - m + end + if r2 > n then + r3 = r3 + 1 + r2 = r2 - m + end + if r3 > n then + r4 = r4 + 1 + r3 = r3 - m + end + if r4 > n then + r5 = r5 + 1 + r4 = r4 - m + end + if r5 > n then + r6 = r6 + 1 + r5 = r5 - m + end + if r6 > n then + r7 = r7 + 1 + r6 = r6 - m + end + if r7 > n then + r8 = r8 + 1 + r7 = r7 - m + end + if r8 > n then + r9 = r9 + 1 + r8 = r8 - m + end + if r9 > n then + r10 = r10 + 1 + r9 = r9 - m + end + if r10 > n then + r11 = r11 + 1 + r10 = r10 - m + end + if r11 > n then + r12 = r12 + 1 + r11 = r11 - m + end + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12} + + return reduce(result) +end + +local function shr(a) + local r1 = a[1] + local r2 = a[2] + local r3 = a[3] + local r4 = a[4] + local r5 = a[5] + local r6 = a[6] + local r7 = a[7] + local r8 = a[8] + local r9 = a[9] + local r10 = a[10] + local r11 = a[11] + local r12 = a[12] + + r1 = r1 / 2 + r1 = r1 - r1 % 1 + r1 = r1 + (r2 % 2) * 0x8000 + r2 = r2 / 2 + r2 = r2 - r2 % 1 + r2 = r2 + (r3 % 2) * 0x8000 + r3 = r3 / 2 + r3 = r3 - r3 % 1 + r3 = r3 + (r4 % 2) * 0x8000 + r4 = r4 / 2 + r4 = r4 - r4 % 1 + r4 = r4 + (r5 % 2) * 0x8000 + r5 = r5 / 2 + r5 = r5 - r5 % 1 + r5 = r5 + (r6 % 2) * 0x8000 + r6 = r6 / 2 + r6 = r6 - r6 % 1 + r6 = r6 + (r7 % 2) * 0x8000 + r7 = r7 / 2 + r7 = r7 - r7 % 1 + r7 = r7 + (r8 % 2) * 0x8000 + r8 = r8 / 2 + r8 = r8 - r8 % 1 + r8 = r8 + (r9 % 2) * 0x8000 + r9 = r9 / 2 + r9 = r9 - r9 % 1 + r9 = r9 + (r10 % 2) * 0x8000 + r10 = r10 / 2 + r10 = r10 - r10 % 1 + r10 = r10 + (r11 % 2) * 0x8000 + r11 = r11 / 2 + r11 = r11 - r11 % 1 + r11 = r11 + (r12 % 2) * 0x8000 + r12 = r12 / 2 + r12 = r12 - r12 % 1 + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12} + + return result +end + +local function sub192(a, b) + local r1 = a[1] - b[1] + local r2 = a[2] - b[2] + local r3 = a[3] - b[3] + local r4 = a[4] - b[4] + local r5 = a[5] - b[5] + local r6 = a[6] - b[6] + local r7 = a[7] - b[7] + local r8 = a[8] - b[8] + local r9 = a[9] - b[9] + local r10 = a[10] - b[10] + local r11 = a[11] - b[11] + local r12 = a[12] - b[12] + + if r1 < 0 then + r2 = r2 - 1 + r1 = r1 + m + end + if r2 < 0 then + r3 = r3 - 1 + r2 = r2 + m + end + if r3 < 0 then + r4 = r4 - 1 + r3 = r3 + m + end + if r4 < 0 then + r5 = r5 - 1 + r4 = r4 + m + end + if r5 < 0 then + r6 = r6 - 1 + r5 = r5 + m + end + if r6 < 0 then + r7 = r7 - 1 + r6 = r6 + m + end + if r7 < 0 then + r8 = r8 - 1 + r7 = r7 + m + end + if r8 < 0 then + r9 = r9 - 1 + r8 = r8 + m + end + if r9 < 0 then + r10 = r10 - 1 + r9 = r9 + m + end + if r10 < 0 then + r11 = r11 - 1 + r10 = r10 + m + end + if r11 < 0 then + r12 = r12 - 1 + r11 = r11 + m + end + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12} + + return result +end + +local function sub(a, b) + local r1 = a[1] - b[1] + local r2 = a[2] - b[2] + local r3 = a[3] - b[3] + local r4 = a[4] - b[4] + local r5 = a[5] - b[5] + local r6 = a[6] - b[6] + local r7 = a[7] - b[7] + local r8 = a[8] - b[8] + local r9 = a[9] - b[9] + local r10 = a[10] - b[10] + local r11 = a[11] - b[11] + local r12 = a[12] - b[12] + + if r1 < 0 then + r2 = r2 - 1 + r1 = r1 + m + end + if r2 < 0 then + r3 = r3 - 1 + r2 = r2 + m + end + if r3 < 0 then + r4 = r4 - 1 + r3 = r3 + m + end + if r4 < 0 then + r5 = r5 - 1 + r4 = r4 + m + end + if r5 < 0 then + r6 = r6 - 1 + r5 = r5 + m + end + if r6 < 0 then + r7 = r7 - 1 + r6 = r6 + m + end + if r7 < 0 then + r8 = r8 - 1 + r7 = r7 + m + end + if r8 < 0 then + r9 = r9 - 1 + r8 = r8 + m + end + if r9 < 0 then + r10 = r10 - 1 + r9 = r9 + m + end + if r10 < 0 then + r11 = r11 - 1 + r10 = r10 + m + end + if r11 < 0 then + r12 = r12 - 1 + r11 = r11 + m + end + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12} + + if r12 < 0 then + result = add(result, p) + end + + return result +end + +local function add384(a, b) + local r1 = a[1] + b[1] + local r2 = a[2] + b[2] + local r3 = a[3] + b[3] + local r4 = a[4] + b[4] + local r5 = a[5] + b[5] + local r6 = a[6] + b[6] + local r7 = a[7] + b[7] + local r8 = a[8] + b[8] + local r9 = a[9] + b[9] + local r10 = a[10] + b[10] + local r11 = a[11] + b[11] + local r12 = a[12] + b[12] + local r13 = a[13] + b[13] + local r14 = a[14] + b[14] + local r15 = a[15] + b[15] + local r16 = a[16] + b[16] + local r17 = a[17] + b[17] + local r18 = a[18] + b[18] + local r19 = a[19] + b[19] + local r20 = a[20] + b[20] + local r21 = a[21] + b[21] + local r22 = a[22] + b[22] + local r23 = a[23] + b[23] + local r24 = a[24] + b[24] + + if r1 > n then + r2 = r2 + 1 + r1 = r1 - m + end + if r2 > n then + r3 = r3 + 1 + r2 = r2 - m + end + if r3 > n then + r4 = r4 + 1 + r3 = r3 - m + end + if r4 > n then + r5 = r5 + 1 + r4 = r4 - m + end + if r5 > n then + r6 = r6 + 1 + r5 = r5 - m + end + if r6 > n then + r7 = r7 + 1 + r6 = r6 - m + end + if r7 > n then + r8 = r8 + 1 + r7 = r7 - m + end + if r8 > n then + r9 = r9 + 1 + r8 = r8 - m + end + if r9 > n then + r10 = r10 + 1 + r9 = r9 - m + end + if r10 > n then + r11 = r11 + 1 + r10 = r10 - m + end + if r11 > n then + r12 = r12 + 1 + r11 = r11 - m + end + if r12 > n then + r13 = r13 + 1 + r12 = r12 - m + end + if r13 > n then + r14 = r14 + 1 + r13 = r13 - m + end + if r14 > n then + r15 = r15 + 1 + r14 = r14 - m + end + if r15 > n then + r16 = r16 + 1 + r15 = r15 - m + end + if r16 > n then + r17 = r17 + 1 + r16 = r16 - m + end + if r17 > n then + r18 = r18 + 1 + r17 = r17 - m + end + if r18 > n then + r19 = r19 + 1 + r18 = r18 - m + end + if r19 > n then + r20 = r20 + 1 + r19 = r19 - m + end + if r20 > n then + r21 = r21 + 1 + r20 = r20 - m + end + if r21 > n then + r22 = r22 + 1 + r21 = r21 - m + end + if r22 > n then + r23 = r23 + 1 + r22 = r22 - m + end + if r23 > n then + r24 = r24 + 1 + r23 = r23 - m + end + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24} + + return result +end + +local function mul384(a, b) + local a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 = unpack(a) + local b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12 = unpack(b) + + local r1 = a1 * b1 + + local r2 = a1 * b2 + r2 = r2 + a2 * b1 + + local r3 = a1 * b3 + r3 = r3 + a2 * b2 + r3 = r3 + a3 * b1 + + local r4 = a1 * b4 + r4 = r4 + a2 * b3 + r4 = r4 + a3 * b2 + r4 = r4 + a4 * b1 + + local r5 = a1 * b5 + r5 = r5 + a2 * b4 + r5 = r5 + a3 * b3 + r5 = r5 + a4 * b2 + r5 = r5 + a5 * b1 + + local r6 = a1 * b6 + r6 = r6 + a2 * b5 + r6 = r6 + a3 * b4 + r6 = r6 + a4 * b3 + r6 = r6 + a5 * b2 + r6 = r6 + a6 * b1 + + local r7 = a1 * b7 + r7 = r7 + a2 * b6 + r7 = r7 + a3 * b5 + r7 = r7 + a4 * b4 + r7 = r7 + a5 * b3 + r7 = r7 + a6 * b2 + r7 = r7 + a7 * b1 + + local r8 = a1 * b8 + r8 = r8 + a2 * b7 + r8 = r8 + a3 * b6 + r8 = r8 + a4 * b5 + r8 = r8 + a5 * b4 + r8 = r8 + a6 * b3 + r8 = r8 + a7 * b2 + r8 = r8 + a8 * b1 + + local r9 = a1 * b9 + r9 = r9 + a2 * b8 + r9 = r9 + a3 * b7 + r9 = r9 + a4 * b6 + r9 = r9 + a5 * b5 + r9 = r9 + a6 * b4 + r9 = r9 + a7 * b3 + r9 = r9 + a8 * b2 + r9 = r9 + a9 * b1 + + local r10 = a1 * b10 + r10 = r10 + a2 * b9 + r10 = r10 + a3 * b8 + r10 = r10 + a4 * b7 + r10 = r10 + a5 * b6 + r10 = r10 + a6 * b5 + r10 = r10 + a7 * b4 + r10 = r10 + a8 * b3 + r10 = r10 + a9 * b2 + r10 = r10 + a10 * b1 + + local r11 = a1 * b11 + r11 = r11 + a2 * b10 + r11 = r11 + a3 * b9 + r11 = r11 + a4 * b8 + r11 = r11 + a5 * b7 + r11 = r11 + a6 * b6 + r11 = r11 + a7 * b5 + r11 = r11 + a8 * b4 + r11 = r11 + a9 * b3 + r11 = r11 + a10 * b2 + r11 = r11 + a11 * b1 + + local r12 = a1 * b12 + r12 = r12 + a2 * b11 + r12 = r12 + a3 * b10 + r12 = r12 + a4 * b9 + r12 = r12 + a5 * b8 + r12 = r12 + a6 * b7 + r12 = r12 + a7 * b6 + r12 = r12 + a8 * b5 + r12 = r12 + a9 * b4 + r12 = r12 + a10 * b3 + r12 = r12 + a11 * b2 + r12 = r12 + a12 * b1 + + local r13 = a2 * b12 + r13 = r13 + a3 * b11 + r13 = r13 + a4 * b10 + r13 = r13 + a5 * b9 + r13 = r13 + a6 * b8 + r13 = r13 + a7 * b7 + r13 = r13 + a8 * b6 + r13 = r13 + a9 * b5 + r13 = r13 + a10 * b4 + r13 = r13 + a11 * b3 + r13 = r13 + a12 * b2 + + local r14 = a3 * b12 + r14 = r14 + a4 * b11 + r14 = r14 + a5 * b10 + r14 = r14 + a6 * b9 + r14 = r14 + a7 * b8 + r14 = r14 + a8 * b7 + r14 = r14 + a9 * b6 + r14 = r14 + a10 * b5 + r14 = r14 + a11 * b4 + r14 = r14 + a12 * b3 + + local r15 = a4 * b12 + r15 = r15 + a5 * b11 + r15 = r15 + a6 * b10 + r15 = r15 + a7 * b9 + r15 = r15 + a8 * b8 + r15 = r15 + a9 * b7 + r15 = r15 + a10 * b6 + r15 = r15 + a11 * b5 + r15 = r15 + a12 * b4 + + local r16 = a5 * b12 + r16 = r16 + a6 * b11 + r16 = r16 + a7 * b10 + r16 = r16 + a8 * b9 + r16 = r16 + a9 * b8 + r16 = r16 + a10 * b7 + r16 = r16 + a11 * b6 + r16 = r16 + a12 * b5 + + local r17 = a6 * b12 + r17 = r17 + a7 * b11 + r17 = r17 + a8 * b10 + r17 = r17 + a9 * b9 + r17 = r17 + a10 * b8 + r17 = r17 + a11 * b7 + r17 = r17 + a12 * b6 + + local r18 = a7 * b12 + r18 = r18 + a8 * b11 + r18 = r18 + a9 * b10 + r18 = r18 + a10 * b9 + r18 = r18 + a11 * b8 + r18 = r18 + a12 * b7 + + local r19 = a8 * b12 + r19 = r19 + a9 * b11 + r19 = r19 + a10 * b10 + r19 = r19 + a11 * b9 + r19 = r19 + a12 * b8 + + local r20 = a9 * b12 + r20 = r20 + a10 * b11 + r20 = r20 + a11 * b10 + r20 = r20 + a12 * b9 + + local r21 = a10 * b12 + r21 = r21 + a11 * b11 + r21 = r21 + a12 * b10 + + local r22 = a11 * b12 + r22 = r22 + a12 * b11 + + local r23 = a12 * b12 + + local r24 = 0 + + r2 = r2 + (r1 / m) + r2 = r2 - r2 % 1 + r1 = r1 % m + r3 = r3 + (r2 / m) + r3 = r3 - r3 % 1 + r2 = r2 % m + r4 = r4 + (r3 / m) + r4 = r4 - r4 % 1 + r3 = r3 % m + r5 = r5 + (r4 / m) + r5 = r5 - r5 % 1 + r4 = r4 % m + r6 = r6 + (r5 / m) + r6 = r6 - r6 % 1 + r5 = r5 % m + r7 = r7 + (r6 / m) + r7 = r7 - r7 % 1 + r6 = r6 % m + r8 = r8 + (r7 / m) + r8 = r8 - r8 % 1 + r7 = r7 % m + r9 = r9 + (r8 / m) + r9 = r9 - r9 % 1 + r8 = r8 % m + r10 = r10 + (r9 / m) + r10 = r10 - r10 % 1 + r9 = r9 % m + r11 = r11 + (r10 / m) + r11 = r11 - r11 % 1 + r10 = r10 % m + r12 = r12 + (r11 / m) + r12 = r12 - r12 % 1 + r11 = r11 % m + r13 = r13 + (r12 / m) + r13 = r13 - r13 % 1 + r12 = r12 % m + r14 = r14 + (r13 / m) + r14 = r14 - r14 % 1 + r13 = r13 % m + r15 = r15 + (r14 / m) + r15 = r15 - r15 % 1 + r14 = r14 % m + r16 = r16 + (r15 / m) + r16 = r16 - r16 % 1 + r15 = r15 % m + r17 = r17 + (r16 / m) + r17 = r17 - r17 % 1 + r16 = r16 % m + r18 = r18 + (r17 / m) + r18 = r18 - r18 % 1 + r17 = r17 % m + r19 = r19 + (r18 / m) + r19 = r19 - r19 % 1 + r18 = r18 % m + r20 = r20 + (r19 / m) + r20 = r20 - r20 % 1 + r19 = r19 % m + r21 = r21 + (r20 / m) + r21 = r21 - r21 % 1 + r20 = r20 % m + r22 = r22 + (r21 / m) + r22 = r22 - r22 % 1 + r21 = r21 % m + r23 = r23 + (r22 / m) + r23 = r23 - r23 % 1 + r22 = r22 % m + r24 = r24 + (r23 / m) + r24 = r24 - r24 % 1 + r23 = r23 % m + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24} + + return result +end + +local function REDC(T) + local m = {unpack(mul384({unpack(T, 1, 12)}, p2), 1, 12)} + local t = {unpack(add384(T, mul384(m, p)), 13, 24)} + + return reduce(t) +end + +local function mul(a, b) + return REDC(mul384(a, b)) +end + +local function sqr(a) + local a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 = unpack(a) + + local r1 = a1 * a1 + + local r2 = a1 * a2 * 2 + + local r3 = a1 * a3 * 2 + r3 = r3 + a2 * a2 + + local r4 = a1 * a4 * 2 + r4 = r4 + a2 * a3 * 2 + + local r5 = a1 * a5 * 2 + r5 = r5 + a2 * a4 * 2 + r5 = r5 + a3 * a3 + + local r6 = a1 * a6 * 2 + r6 = r6 + a2 * a5 * 2 + r6 = r6 + a3 * a4 * 2 + + local r7 = a1 * a7 * 2 + r7 = r7 + a2 * a6 * 2 + r7 = r7 + a3 * a5 * 2 + r7 = r7 + a4 * a4 + + local r8 = a1 * a8 * 2 + r8 = r8 + a2 * a7 * 2 + r8 = r8 + a3 * a6 * 2 + r8 = r8 + a4 * a5 * 2 + + local r9 = a1 * a9 * 2 + r9 = r9 + a2 * a8 * 2 + r9 = r9 + a3 * a7 * 2 + r9 = r9 + a4 * a6 * 2 + r9 = r9 + a5 * a5 + + local r10 = a1 * a10 * 2 + r10 = r10 + a2 * a9 * 2 + r10 = r10 + a3 * a8 * 2 + r10 = r10 + a4 * a7 * 2 + r10 = r10 + a5 * a6 * 2 + + local r11 = a1 * a11 * 2 + r11 = r11 + a2 * a10 * 2 + r11 = r11 + a3 * a9 * 2 + r11 = r11 + a4 * a8 * 2 + r11 = r11 + a5 * a7 * 2 + r11 = r11 + a6 * a6 + + local r12 = a1 * a12 * 2 + r12 = r12 + a2 * a11 * 2 + r12 = r12 + a3 * a10 * 2 + r12 = r12 + a4 * a9 * 2 + r12 = r12 + a5 * a8 * 2 + r12 = r12 + a6 * a7 * 2 + + local r13 = a2 * a12 * 2 + r13 = r13 + a3 * a11 * 2 + r13 = r13 + a4 * a10 * 2 + r13 = r13 + a5 * a9 * 2 + r13 = r13 + a6 * a8 * 2 + r13 = r13 + a7 * a7 + + local r14 = a3 * a12 * 2 + r14 = r14 + a4 * a11 * 2 + r14 = r14 + a5 * a10 * 2 + r14 = r14 + a6 * a9 * 2 + r14 = r14 + a7 * a8 * 2 + + local r15 = a4 * a12 * 2 + r15 = r15 + a5 * a11 * 2 + r15 = r15 + a6 * a10 * 2 + r15 = r15 + a7 * a9 * 2 + r15 = r15 + a8 * a8 + + local r16 = a5 * a12 * 2 + r16 = r16 + a6 * a11 * 2 + r16 = r16 + a7 * a10 * 2 + r16 = r16 + a8 * a9 * 2 + + local r17 = a6 * a12 * 2 + r17 = r17 + a7 * a11 * 2 + r17 = r17 + a8 * a10 * 2 + r17 = r17 + a9 * a9 + + local r18 = a7 * a12 * 2 + r18 = r18 + a8 * a11 * 2 + r18 = r18 + a9 * a10 * 2 + + local r19 = a8 * a12 * 2 + r19 = r19 + a9 * a11 * 2 + r19 = r19 + a10 * a10 + + local r20 = a9 * a12 * 2 + r20 = r20 + a10 * a11 * 2 + + local r21 = a10 * a12 * 2 + r21 = r21 + a11 * a11 + + local r22 = a11 * a12 * 2 + + local r23 = a12 * a12 + + local r24 = 0 + + r2 = r2 + (r1 / m) + r2 = r2 - r2 % 1 + r1 = r1 % m + r3 = r3 + (r2 / m) + r3 = r3 - r3 % 1 + r2 = r2 % m + r4 = r4 + (r3 / m) + r4 = r4 - r4 % 1 + r3 = r3 % m + r5 = r5 + (r4 / m) + r5 = r5 - r5 % 1 + r4 = r4 % m + r6 = r6 + (r5 / m) + r6 = r6 - r6 % 1 + r5 = r5 % m + r7 = r7 + (r6 / m) + r7 = r7 - r7 % 1 + r6 = r6 % m + r8 = r8 + (r7 / m) + r8 = r8 - r8 % 1 + r7 = r7 % m + r9 = r9 + (r8 / m) + r9 = r9 - r9 % 1 + r8 = r8 % m + r10 = r10 + (r9 / m) + r10 = r10 - r10 % 1 + r9 = r9 % m + r11 = r11 + (r10 / m) + r11 = r11 - r11 % 1 + r10 = r10 % m + r12 = r12 + (r11 / m) + r12 = r12 - r12 % 1 + r11 = r11 % m + r13 = r13 + (r12 / m) + r13 = r13 - r13 % 1 + r12 = r12 % m + r14 = r14 + (r13 / m) + r14 = r14 - r14 % 1 + r13 = r13 % m + r15 = r15 + (r14 / m) + r15 = r15 - r15 % 1 + r14 = r14 % m + r16 = r16 + (r15 / m) + r16 = r16 - r16 % 1 + r15 = r15 % m + r17 = r17 + (r16 / m) + r17 = r17 - r17 % 1 + r16 = r16 % m + r18 = r18 + (r17 / m) + r18 = r18 - r18 % 1 + r17 = r17 % m + r19 = r19 + (r18 / m) + r19 = r19 - r19 % 1 + r18 = r18 % m + r20 = r20 + (r19 / m) + r20 = r20 - r20 % 1 + r19 = r19 % m + r21 = r21 + (r20 / m) + r21 = r21 - r21 % 1 + r20 = r20 % m + r22 = r22 + (r21 / m) + r22 = r22 - r22 % 1 + r21 = r21 % m + r23 = r23 + (r22 / m) + r23 = r23 - r23 % 1 + r22 = r22 % m + r24 = r24 + (r23 / m) + r24 = r24 - r24 % 1 + r23 = r23 % m + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24} + + return REDC(result) +end + +local function mont(a) + return mul(a, r2) +end + +local function invMont(a) + local a = {unpack(a)} + + for i = 13, 24 do + a[i] = 0 + end + + return REDC(a) +end + +return { + eq = eq, + mul = mul, + sqr = sqr, + add = add, + sub = sub, + shr = shr, + mont = mont, + invMont = invMont, + sub192 = sub192 +} +end +preload["empty"] = function(...) + +end +preload["elliptic"] = function(...) +---- Elliptic Curve Arithmetic + +---- About the Curve Itself +-- Field Size: 192 bits +-- Field Modulus (p): 65533 * 2^176 + 3 +-- Equation: x^2 + y^2 = 1 + 108 * x^2 * y^2 +-- Parameters: Edwards Curve with c = 1, and d = 108 +-- Curve Order (n): 4 * 1569203598118192102418711808268118358122924911136798015831 +-- Cofactor (h): 4 +-- Generator Order (q): 1569203598118192102418711808268118358122924911136798015831 +---- About the Curve's Security +-- Current best attack security: 94.822 bits (Pollard's Rho) +-- Rho Security: log2(0.884 * sqrt(q)) = 94.822 +-- Transfer Security? Yes: p ~= q; k > 20 +-- Field Discriminant Security? Yes: t = 67602300638727286331433024168; s = 2^2; |D| = 5134296629560551493299993292204775496868940529592107064435 > 2^100 +-- Rigidity? A little, the parameters are somewhat small. +-- XZ/YZ Ladder Security? No: Single coordinate ladders are insecure, so they can't be used. +-- Small Subgroup Security? Yes: Secret keys are calculated modulo 4q. +-- Invalid Curve Security? Yes: Any point to be multiplied is checked beforehand. +-- Invalid Curve Twist Security? No: The curve is not protected against single coordinate ladder attacks, so don't use them. +-- Completeness? Yes: The curve is an Edwards Curve with non-square d and square a, so the curve is complete. +-- Indistinguishability? No: The curve does not support indistinguishability maps. + +fp = irequire("fp") + +local eq = fp.eq +local mul = fp.mul +local sqr = fp.sqr +local add = fp.add +local sub = fp.sub +local shr = fp.shr +local mont = fp.mont +local invMont = fp.invMont +local sub192 = fp.sub192 + +local bits = 192 +local pMinusTwoBinary = {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} +local pMinusThreeOverFourBinary = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0} +local ZERO = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +local ONE = mont({1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) + +local p = mont({3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65533}) +local G = { + mont({30457, 58187, 5603, 63215, 8936, 58151, 26571, 7272, 26680, 23486, 32353, 59456}), + mont({3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}), + mont({1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) +} +local GTable = {G} + +local d = mont({108, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) + +local bxor = bit32.bxor or bit.bxor +local mt = { + __tostring = function(a) return string.char(unpack(a)) end, + __index = { + toHex = function(self, s) return ("%02x"):rep(#self):format(unpack(self)) end, + isEqual = function(self, t) + if type(t) ~= "table" then return false end + if #self ~= #t then return false end + local ret = 0 + for i = 1, #self do + ret = bit32.bor(ret, bxor(self[i], t[i])) + end + return ret == 0 + end + } +} + +local function expMod(a, t) + local a = {unpack(a)} + local result = {unpack(ONE)} + + for i = 1, bits do + if t[i] == 1 then + result = mul(result, a) + end + a = mul(a, a) + end + + return result +end + +-- We're using Projective Coordinates +-- For Edwards curves +-- The identity element is represented by (0:1:1) +local function pointDouble(P1) + local X1, Y1, Z1 = unpack(P1) + + local b = add(X1, Y1) + local B = sqr(b) + local C = sqr(X1) + local D = sqr(Y1) + local E = add(C, D) + local H = sqr(Z1) + local J = sub(E, add(H, H)) + local X3 = mul(sub(B, E), J) + local Y3 = mul(E, sub(C, D)) + local Z3 = mul(E, J) + + local P3 = {X3, Y3, Z3} + + return P3 +end + +local function pointAdd(P1, P2) + local X1, Y1, Z1 = unpack(P1) + local X2, Y2, Z2 = unpack(P2) + + local A = mul(Z1, Z2) + local B = sqr(A) + local C = mul(X1, X2) + local D = mul(Y1, Y2) + local E = mul(d, mul(C, D)) + local F = sub(B, E) + local G = add(B, E) + local X3 = mul(A, mul(F, sub(mul(add(X1, Y1), add(X2, Y2)), add(C, D)))) + local Y3 = mul(A, mul(G, sub(D, C))) + local Z3 = mul(F, G) + + local P3 = {X3, Y3, Z3} + + return P3 +end + +local function pointNeg(P1) + local X1, Y1, Z1 = unpack(P1) + + local X3 = sub(p, X1) + local Y3 = {unpack(Y1)} + local Z3 = {unpack(Z1)} + + local P3 = {X3, Y3, Z3} + + return P3 +end + +local function pointSub(P1, P2) + return pointAdd(P1, pointNeg(P2)) +end + +local function pointScale(P1) + local X1, Y1, Z1 = unpack(P1) + + local A = expMod(Z1, pMinusTwoBinary) + local X3 = mul(X1, A) + local Y3 = mul(Y1, A) + local Z3 = {unpack(ONE)} + + local P3 = {X3, Y3, Z3} + + return P3 +end + +local function pointEq(P1, P2) + local X1, Y1, Z1 = unpack(P1) + local X2, Y2, Z2 = unpack(P2) + + local A1 = mul(X1, Z2) + local B1 = mul(Y1, Z2) + local A2 = mul(X2, Z1) + local B2 = mul(Y2, Z1) + + return eq(A1, A2) and eq(B1, B2) +end + +local function pointIsOnCurve(P1) + local X1, Y1, Z1 = unpack(P1) + + local X12 = sqr(X1) + local Y12 = sqr(Y1) + local Z12 = sqr(Z1) + local Z14 = sqr(Z12) + local a = add(X12, Y12) + a = mul(a, Z12) + local b = mul(d, mul(X12, Y12)) + b = add(Z14, b) + + return eq(a, b) +end + +local function mods(d) + -- w = 5 + local result = d[1] % 32 + + if result >= 16 then + result = result - 32 + end + + return result +end + +local function NAF(d) + local t = {} + local d = {unpack(d)} + + while d[12] >= 0 and not eq(d, ZERO) do + if d[1] % 2 == 1 then + t[#t + 1] = mods(d) + d = sub192(d, {t[#t], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) + else + t[#t + 1] = 0 + end + + d = shr(d) + end + + return t +end + +local function scalarMul(s, P1) + local naf = NAF(s) + local PTable = {P1} + local P2 = pointDouble(P1) + + for i = 3, 31, 2 do + PTable[i] = pointAdd(PTable[i - 2], P2) + end + + local Q = {{unpack(ZERO)}, {unpack(ONE)}, {unpack(ONE)}} + for i = #naf, 1, -1 do + Q = pointDouble(Q) + if naf[i] > 0 then + Q = pointAdd(Q, PTable[naf[i]]) + elseif naf[i] < 0 then + Q = pointSub(Q, PTable[-naf[i]]) + end + end + + return Q +end + +for i = 2, 196 do + GTable[i] = pointDouble(GTable[i - 1]) +end + +local function scalarMulG(s) + local result = {{unpack(ZERO)}, {unpack(ONE)}, {unpack(ONE)}} + local k = 1 + + for i = 1, 12 do + local w = s[i] + + for j = 1, 16 do + if w % 2 == 1 then + result = pointAdd(result, GTable[k]) + end + + k = k + 1 + + w = w / 2 + w = w - w % 1 + end + end + + return result +end + +local function pointEncode(P1) + P1 = pointScale(P1) + + local result = {} + local x, y = unpack(P1) + + result[1] = x[1] % 2 + + for i = 1, 12 do + local m = y[i] % 256 + result[2 * i] = m + result[2 * i + 1] = (y[i] - m) / 256 + end + + return setmetatable(result, mt) +end + +local function pointDecode(enc) + local y = {} + for i = 1, 12 do + y[i] = enc[2 * i] + y[i] = y[i] + enc[2 * i + 1] * 256 + end + + local y2 = sqr(y) + local u = sub(y2, ONE) + local v = sub(mul(d, y2), ONE) + local u2 = sqr(u) + local u3 = mul(u, u2) + local u5 = mul(u3, u2) + local v3 = mul(v, sqr(v)) + local w = mul(u5, v3) + local x = mul(u3, mul(v, expMod(w, pMinusThreeOverFourBinary))) + + if x[1] % 2 ~= enc[1] then + x = sub(p, x) + end + + local P3 = {x, y, {unpack(ONE)}} + + return P3 +end + +return { + G = G, + pointAdd = pointAdd, + pointNeg = pointNeg, + pointSub = pointSub, + pointEq = pointEq, + pointIsOnCurve = pointIsOnCurve, + scalarMul = scalarMul, + scalarMulG = scalarMulG, + pointEncode = pointEncode, + pointDecode = pointDecode +} +end +preload["ecc"] = function(...) +local fq = irequire("fq") +local elliptic = irequire("elliptic") +local sha256 = require("sha256") +require("urandom") + +local q = {1372, 62520, 47765, 8105, 45059, 9616, 65535, 65535, 65535, 65535, 65535, 65532} + +local sLen = 24 +local eLen = 24 + +local function hashModQ(sk) + local hash = sha256.hmac({0x00}, sk) + local x + repeat + hash = sha256.digest(hash) + x = fq.fromBytes(hash) + until fq.cmp(x, q) <= 0 + + return x +end + +local function publicKey(sk) + local x = hashModQ(sk) + + local Y = elliptic.scalarMulG(x) + local pk = elliptic.pointEncode(Y) + + return pk +end + +local function keypair() + local priv = os.urandom() + local pub = publicKey(priv) + return pub, priv +end + +local function exchange(sk, pk) + local Y = elliptic.pointDecode(pk) + local x = hashModQ(sk) + + local Z = elliptic.scalarMul(x, Y) + Z = elliptic.pointScale(Z) + + local ss = fq.bytes(Z[2]) + local ss = sha256.digest(ss) + + return ss +end + +local function sign(sk, message) + message = type(message) == "table" and string.char(unpack(message)) or message + sk = type(sk) == "table" and string.char(unpack(sk)) or sk + local epoch = tostring(os.epoch("utc")) + local x = hashModQ(sk) + local k = hashModQ(message .. epoch .. sk) + + local R = elliptic.scalarMulG(k) + R = string.char(unpack(elliptic.pointEncode(R))) + local e = hashModQ(R .. message) + local s = fq.sub(k, fq.mul(x, e)) + + e = fq.bytes(e) + s = fq.bytes(s) + + local sig = e + + for i = 1, #s do + sig[#sig + 1] = s[i] + end + + return sig +end + +local function verify(pk, message, sig) + local Y = elliptic.pointDecode(pk) + local e = {unpack(sig, 1, eLen)} + local s = {unpack(sig, eLen + 1, eLen + sLen)} + + e = fq.fromBytes(e) + s = fq.fromBytes(s) + + local R = elliptic.pointAdd(elliptic.scalarMulG(s), elliptic.scalarMul(e, Y)) + R = string.char(unpack(elliptic.pointEncode(R))) + local e2 = hashModQ(R .. message) + + return fq.eq(e2, e) +end + +return { + publicKey = publicKey, + exchange = exchange, + sign = sign, + verify = verify, + keypair = keypair +} +end +return irequire \ No newline at end of file diff --git a/src/lib/gps.lua b/src/lib/gps.lua new file mode 100644 index 0000000..0d457e9 --- /dev/null +++ b/src/lib/gps.lua @@ -0,0 +1,213 @@ +local expect +if _G["~expect"] then + expect = _G["~expect"] +else + local native_select, native_type = select, type + expect = function(index, value, ...) + local t = native_type(value) + for i = 1, native_select("#", ...) do + if t == native_select(i, ...) then return true end + end + local types = table.pack(...) + for i = types.n, 1, -1 do + if types[i] == "nil" then table.remove(types, i) end + end + local type_names + if #types <= 1 then + type_names = tostring(...) + else + type_names = table.concat(types, ", ", 1, #types - 1) .. " or " .. types[#types] + end + -- If we can determine the function name with a high level of confidence, try to include it. + local name + if native_type(debug) == "table" and native_type(debug.getinfo) == "function" then + local ok, info = pcall(debug.getinfo, 3, "nS") + if ok and info.name and #info.name ~= "" and info.what ~= "C" then name = info.name end + end + if name then + error( ("bad argument #%d to '%s' (expected %s, got %s)"):format(index, name, type_names, t), 3 ) + else + error( ("bad argument #%d (expected %s, got %s)"):format(index, type_names, t), 3 ) + end + end +end + +CHANNEL_GPS = 65534 + +local function trilaterate( A, B, C ) + local a2b = B.vPosition - A.vPosition + local a2c = C.vPosition - A.vPosition + + if math.abs( a2b:normalize():dot( a2c:normalize() ) ) > 0.999 then + return nil + end + + local d = a2b:length() + local ex = a2b:normalize( ) + local i = ex:dot( a2c ) + local ey = (a2c - ex * i):normalize() + local j = ey:dot( a2c ) + local ez = ex:cross( ey ) + + local r1 = A.nDistance + local r2 = B.nDistance + local r3 = C.nDistance + + local x = (r1 * r1 - r2 * r2 + d * d) / (2 * d) + local y = (r1 * r1 - r3 * r3 - x * x + (x - i) * (x - i) + j * j) / (2 * j) + + local result = A.vPosition + ex * x + ey * y + + local zSquared = r1 * r1 - x * x - y * y + if zSquared > 0 then + local z = math.sqrt( zSquared ) + local result1 = result + ez * z + local result2 = result - ez * z + + local rounded1, rounded2 = result1:round( 0.01 ), result2:round( 0.01 ) + if rounded1.x ~= rounded2.x or rounded1.y ~= rounded2.y or rounded1.z ~= rounded2.z then + return rounded1, rounded2 + else + return rounded1 + end + end + return result:round( 0.01 ) + +end + +local function narrow( p1, p2, fix ) + local dist1 = math.abs( (p1 - fix.vPosition):length() - fix.nDistance ) + local dist2 = math.abs( (p2 - fix.vPosition):length() - fix.nDistance ) + + if math.abs(dist1 - dist2) < 0.01 then + return p1, p2 + elseif dist1 < dist2 then + return p1:round( 0.01 ) + else + return p2:round( 0.01 ) + end +end + +function locate( _nTimeout, _bDebug ) + expect(1, _nTimeout, "number", "nil") + expect(2, _bDebug, "boolean", "nil") + -- Let command computers use their magic fourth-wall-breaking special abilities + if commands then + return commands.getBlockPosition() + end + + -- Find a modem + local sModemSide = nil + for _, sSide in ipairs( rs.getSides() ) do + if peripheral.getType( sSide ) == "modem" and peripheral.call( sSide, "isWireless" ) then + sModemSide = sSide + break + end + end + + if sModemSide == nil then + if _bDebug then + print( "No wireless modem attached" ) + end + return nil + end + + if _bDebug then + print( "Finding position..." ) + end + + -- Open it + local modem = peripheral.wrap( sModemSide ) + local bCloseChannel = false + if not modem.isOpen( CHANNEL_GPS ) then + modem.open( CHANNEL_GPS ) + bCloseChannel = true + end + + -- Send a ping to listening GPS hosts + modem.transmit( CHANNEL_GPS, CHANNEL_GPS, "PING" ) + + -- Wait for the responses + local tFixes = {} + local pos1, pos2, dimension + local timeout = os.startTimer( _nTimeout or 2 ) + while true do + local e, p1, p2, p3, p4, p5 = os.pullEvent() + if e == "modem_message" then + -- We received a reply from a modem + local sSide, sChannel, sReplyChannel, tMessage, nDistance = p1, p2, p3, p4, p5 + if sSide == sModemSide and sChannel == CHANNEL_GPS and sReplyChannel == CHANNEL_GPS and nDistance then + -- Received the correct message from the correct modem: use it to determine position + if type(tMessage) == "table" and #tMessage == 3 and tonumber(tMessage[1]) and tonumber(tMessage[2]) and tonumber(tMessage[3]) then + local tFix = { vPosition = vector.new( tMessage[1], tMessage[2], tMessage[3] ), nDistance = nDistance, dimension = tMessage.dimension } + if _bDebug then + local text = tFix.nDistance .. " metres from " .. tostring(tFix.vPosition) + if type(tFix.dimension) == "number" or type(tFix.dimension) == "string" then text = text .. " " .. tFix.dimension end + print(text) + end + if tFix.nDistance == 0 then + pos1, pos2 = tFix.vPosition, nil + else + table.insert( tFixes, tFix ) + if #tFixes >= 3 then + if not pos1 then + pos1, pos2 = trilaterate( tFixes[1], tFixes[2], tFixes[#tFixes] ) + else + pos1, pos2 = narrow( pos1, pos2, tFixes[#tFixes] ) + end + end + end + if pos1 and not pos2 then + local dimension_claims = {} + for _, fix in pairs(tFixes) do + if type(fix.dimension) == "string" or type(fix.dimension) == "number" then + dimension_claims[fix.dimension] = (dimension_claims[fix.dimension] or 0) + 1 + end + end + local highest_count = 0 + for dim, count in pairs(dimension_claims) do + if count > highest_count then + highest_count = count + dimension = dim + end + end + break + end + end + end + + elseif e == "timer" then + -- We received a timeout + local timer = p1 + if timer == timeout then + break + end + + end + end + + -- Close the channel, if we opened one + if bCloseChannel then + modem.close( CHANNEL_GPS ) + end + + -- Return the response + if pos1 and pos2 then + if _bDebug then + print( "Ambiguous position" ) + print( "Could be " .. pos1.x .. "," .. pos1.y .. "," .. pos1.z .. " or " .. pos2.x .. "," .. pos2.y .. "," .. pos2.z ) + end + return nil + elseif pos1 then + if _bDebug then + print( "Position is " .. pos1.x .. "," .. pos1.y .. "," .. pos1.z ) + if dimension then print("Dimension is " .. dimension) end + end + return pos1.x, pos1.y, pos1.z, dimension + else + if _bDebug then + print( "Could not determine position" ) + end + return nil + end +end \ No newline at end of file diff --git a/src/lib/json.lua b/src/lib/json.lua new file mode 100644 index 0000000..75ed79c --- /dev/null +++ b/src/lib/json.lua @@ -0,0 +1,400 @@ +-- +-- json.lua +-- +-- Copyright (c) 2018 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = {} + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\\\", + [ "\"" ] = "\\\"", + [ "\b" ] = "\\b", + [ "\f" ] = "\\f", + [ "\n" ] = "\\n", + [ "\r" ] = "\\r", + [ "\t" ] = "\\t", +} + +local escape_char_map_inv = { [ "\\/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return escape_char_map[c] or string.format("\\u%04x", c:byte()) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if val[1] ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(3, 6), 16 ) + local n2 = tonumber( s:sub(9, 12), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local has_unicode_escape = false + local has_surrogate_escape = false + local has_escape = false + local last + for j = i + 1, #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + end + + if last == 92 then -- "\\" (escape char) + if x == 117 then -- "u" (unicode escape sequence) + local hex = str:sub(j + 1, j + 5) + if not hex:find("%x%x%x%x") then + decode_error(str, j, "invalid unicode escape in string") + end + if hex:find("^[dD][89aAbB]") then + has_surrogate_escape = true + else + has_unicode_escape = true + end + else + local c = string.char(x) + if not escape_chars[c] then + decode_error(str, j, "invalid escape char '" .. c .. "' in string") + end + has_escape = true + end + last = nil + + elseif x == 34 then -- '"' (end of string) + local s = str:sub(i + 1, j - 1) + if has_surrogate_escape then + s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) + end + if has_unicode_escape then + s = s:gsub("\\u....", parse_unicode_escape) + end + if has_escape then + s = s:gsub("\\.", escape_char_map_inv) + end + return s, j + 1 + + else + last = x + end + end + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json diff --git a/src/lib/meta.lua b/src/lib/meta.lua new file mode 100644 index 0000000..6cb08d5 --- /dev/null +++ b/src/lib/meta.lua @@ -0,0 +1,28 @@ +local function new() + local x = { level = 1, new = new } + local m = {} + setmetatable(x, m) + + m.__eq = function(p1,p2) + if getmetatable(p1) == getmetatable(p2) then + return true + end + end + + m.__index = function(inst, key) + local lvl = rawget(inst, "level") + if key == "level" then + return lvl + else + return setmetatable({ level = lvl + 1 }, m) + end + end + + m.__tostring = function(inst) + return ("RECURSION "):rep(rawget(inst, "level")) + end + + return x +end + +return new() \ No newline at end of file diff --git a/src/lib/persistence.lua b/src/lib/persistence.lua new file mode 100644 index 0000000..399c233 --- /dev/null +++ b/src/lib/persistence.lua @@ -0,0 +1,49 @@ +--[[ +Fixes PS#DE7E9F99 +Out-of-sandbox code/data files could be overwritten using this due to environment weirdness. +Now persistence data is in its own folder so this will not happen. +]] +local persist_dir = "persistence_data" + +local function load_data(name) + local file = fs.combine(persist_dir, name) + if not fs.exists(file) then error "does not exist" end + local f = fs.open(file, "r") + local x = f.readAll() + f.close() + return textutils.unserialise(x) +end + +local function save_data(name, value) + if not fs.isDir(persist_dir) then fs.delete(persist_dir) fs.makeDir(persist_dir) end + local f = fs.open(fs.combine(persist_dir, name), "w") + f.write(textutils.serialise(value)) + f.close() +end + +return function(name) + if type(name) ~= "string" then error "Name of persistence volume must be a string." end + local ok, data = pcall(load_data, name) + if not ok or not data then data = {} end + local mt = {} + setmetatable(data, mt) + + mt.__index = { + save = function() + save_data(name, data) + end, + reload = function() + -- swap table in place to keep references valid + for k in pairs(data) do data[k] = nil end + for k, v in pairs(load_data(name)) do + data[k] = v + end + end + } + + function mt.__tostring(_) + return ("[PersistenceStore %s]"):format(name) + end + + return data +end \ No newline at end of file diff --git a/src/lib/registry.lua b/src/lib/registry.lua new file mode 100644 index 0000000..30e2d89 --- /dev/null +++ b/src/lib/registry.lua @@ -0,0 +1,72 @@ +local ser = require "binary-serialization" + +function split(self, separator, max) + local out = {} + if self:len() > 0 then + max = max or -1 + + local field, start = 1, 1 + local first, last = self:find(separator, start, true) + while first and max ~= 0 do + out[field] = self:sub(start, first - 1) + field = field + 1 + start = last + 1 + first , last = self:find(separator, start, true) + max = max - 1 + end + out[field] = self:sub(start) + end + + return out +end + +local function fwrite(n, c) + local f = fs.open(n, "wb") + f.write(c) + f.close() +end + +local function fread(n) + local f = fs.open(n, "rb") + local out = f.readAll() + f.close() + return out +end + +local fsopen = fs.open +local registry_path = ".registry" +local registry = {} + +function registry.set(key, value) + local path = split(key, ".") + local ok, orig_data + if fs.exists(registry_path) then + ok, orig_data = pcall(ser.deserialize, fread(registry_path)) + if not ok then orig_data = {} end + else + orig_data = {} + end + local last_bit = table.remove(path) + local data = orig_data + for _, seg in ipairs(path) do + local new_bit = data[seg] + if type(new_bit) ~= "table" then data[seg] = {} end + data = data[seg] + end + data[last_bit] = value + fwrite(registry_path, ser.serialize(orig_data)) +end + +function registry.get(key) + if not fs.exists(registry_path) then return nil end + local path = split(key, ".") + local ok, data = pcall(ser.deserialize, fread(registry_path)) + if not ok then data = {} end + for _, seg in ipairs(path) do + data = data[seg] + if not data then return nil end + end + return data +end + +return registry \ No newline at end of file diff --git a/src/lib/sha256.lua b/src/lib/sha256.lua new file mode 100644 index 0000000..7a73582 --- /dev/null +++ b/src/lib/sha256.lua @@ -0,0 +1,172 @@ +-- SHA-256, HMAC and PBKDF2 functions in ComputerCraft +-- By Anavrins +-- MIT License +-- Pastebin: https://pastebin.com/6UV4qfNF +-- Usage: https://pastebin.com/q2SQ7eRg +-- Last updated: March 27 2020 + +-- HMAC/PBKFD2 removed + +local mod32 = 2^32 +local band = bit32 and bit32.band or bit.band +local bnot = bit32 and bit32.bnot or bit.bnot +local bxor = bit32 and bit32.bxor or bit.bxor +local blshift = bit32 and bit32.lshift or bit.blshift +local upack = unpack + +local function rrotate(n, b) + local s = n/(2^b) + local f = s%1 + return (s-f) + f*mod32 +end +local function brshift(int, by) + local s = int / (2^by) + return s - s%1 +end + +local H = { + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, +} + +local K = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, +} + +local function counter(incr) + local t1, t2 = 0, 0 + if 0xFFFFFFFF - t1 < incr then + t2 = t2 + 1 + t1 = incr - (0xFFFFFFFF - t1) - 1 + else t1 = t1 + incr + end + return t2, t1 +end + +local function BE_toInt(bs, i) + return blshift((bs[i] or 0), 24) + blshift((bs[i+1] or 0), 16) + blshift((bs[i+2] or 0), 8) + (bs[i+3] or 0) +end + +local function preprocess(data) + local len = #data + local proc = {} + data[#data+1] = 0x80 + while #data%64~=56 do data[#data+1] = 0 end + local blocks = math.ceil(#data/64) + for i = 1, blocks do + proc[i] = {} + for j = 1, 16 do + proc[i][j] = BE_toInt(data, 1+((i-1)*64)+((j-1)*4)) + end + end + proc[blocks][15], proc[blocks][16] = counter(len*8) + return proc +end + +local function digestblock(w, C) + for j = 17, 64 do + local v = w[j-15] + local s0 = bxor(rrotate(w[j-15], 7), rrotate(w[j-15], 18), brshift(w[j-15], 3)) + local s1 = bxor(rrotate(w[j-2], 17), rrotate(w[j-2], 19),brshift(w[j-2], 10)) + w[j] = (w[j-16] + s0 + w[j-7] + s1)%mod32 + end + local a, b, c, d, e, f, g, h = upack(C) + for j = 1, 64 do + local S1 = bxor(rrotate(e, 6), rrotate(e, 11), rrotate(e, 25)) + local ch = bxor(band(e, f), band(bnot(e), g)) + local temp1 = (h + S1 + ch + K[j] + w[j])%mod32 + local S0 = bxor(rrotate(a, 2), rrotate(a, 13), rrotate(a, 22)) + local maj = bxor(bxor(band(a, b), band(a, c)), band(b, c)) + local temp2 = (S0 + maj)%mod32 + h, g, f, e, d, c, b, a = g, f, e, (d+temp1)%mod32, c, b, a, (temp1+temp2)%mod32 + end + C[1] = (C[1] + a)%mod32 + C[2] = (C[2] + b)%mod32 + C[3] = (C[3] + c)%mod32 + C[4] = (C[4] + d)%mod32 + C[5] = (C[5] + e)%mod32 + C[6] = (C[6] + f)%mod32 + C[7] = (C[7] + g)%mod32 + C[8] = (C[8] + h)%mod32 + return C +end + +local mt = { + __tostring = function(a) return string.char(unpack(a)) end, + __index = { + toHex = function(self, s) return ("%02x"):rep(#self):format(unpack(self)) end, + isEqual = function(self, t) + if type(t) ~= "table" then return false end + if #self ~= #t then return false end + local ret = 0 + for i = 1, #self do + ret = bit32.bor(ret, bxor(self[i], t[i])) + end + return ret == 0 + end + } +} + +local function toBytes(t, n) + local b = {} + for i = 1, n do + b[(i-1)*4+1] = band(brshift(t[i], 24), 0xFF) + b[(i-1)*4+2] = band(brshift(t[i], 16), 0xFF) + b[(i-1)*4+3] = band(brshift(t[i], 8), 0xFF) + b[(i-1)*4+4] = band(t[i], 0xFF) + end + return setmetatable(b, mt) +end + +local function digest(data) + local data = data or "" + data = type(data) == "table" and {upack(data)} or {tostring(data):byte(1,-1)} + + data = preprocess(data) + local C = {upack(H)} + for i = 1, #data do C = digestblock(data[i], C) end + return toBytes(C, 8) +end + +local function hmac(data, key) + local data = type(data) == "table" and {upack(data)} or {tostring(data):byte(1,-1)} + local key = type(key) == "table" and {upack(key)} or {tostring(key):byte(1,-1)} + + local blocksize = 64 + + key = #key > blocksize and digest(key) or key + + local ipad = {} + local opad = {} + local padded_key = {} + + for i = 1, blocksize do + ipad[i] = bxor(0x36, key[i] or 0) + opad[i] = bxor(0x5C, key[i] or 0) + end + + for i = 1, #data do + ipad[blocksize+i] = data[i] + end + + ipad = digest(ipad) + + for i = 1, blocksize do + padded_key[i] = opad[i] + padded_key[blocksize+i] = ipad[i] + end + + return digest(padded_key) +end + +return { + hmac = hmac, + digest = digest +} \ No newline at end of file diff --git a/src/lib/stack_trace.lua b/src/lib/stack_trace.lua new file mode 100644 index 0000000..cf6c72a --- /dev/null +++ b/src/lib/stack_trace.lua @@ -0,0 +1,94 @@ +-- see pastebin.com/NdUKJ07j for license info +-- mostly written by SquidDev, bodged by gollark/osmarks + +local type = type +local debug_traceback = type(debug) == "table" and type(debug.traceback) == "function" and debug.traceback + +local _pcall, _xpcall = pcall, xpcall + +local function traceback(x) + -- Attempt to detect error() and error("xyz", 0). + -- This probably means they're erroring the program intentionally and so we + -- shouldn't display anything. + if x == nil or (type(x) == "string" and not x:find(":%d+:")) or type(x) ~= "string" then + return x + end + + if debug_traceback then + -- The parens are important, as they prevent a tail call occuring, meaning + -- the stack level is preserved. This ensures the code behaves identically + -- on LuaJ and PUC Lua. + return (debug_traceback(tostring(x), 2)) + else + local level = 3 + local out = { tostring(x), "stack traceback:" } + while true do + local _, msg = _pcall(error, "", level) + if msg == "" then break end + + out[#out + 1] = " " .. msg + level = level + 1 + end + + return table.concat(out, "\n") + end +end + +local function trim_traceback(target, marker) + local target = tostring(target) + local ttarget, tmarker = {}, {} + for line in target:gmatch("([^\n]*)\n?") do ttarget[#ttarget + 1] = line end + for line in marker:gmatch("([^\n]*)\n?") do tmarker[#tmarker + 1] = line end + + -- Trim identical suffixes + local t_len, m_len = #ttarget, #tmarker + while t_len >= 3 and ttarget[t_len] == tmarker[m_len] do + table.remove(ttarget, t_len) + t_len, m_len = t_len - 1, m_len - 1 + end + + -- Trim elements from this file and xpcall invocations + while t_len >= 1 and ttarget[t_len]:find("^\tstack_trace%.lua:%d+:") or + ttarget[t_len] == "\t[C]: in function 'xpcall'" or ttarget[t_len] == " xpcall: " do + table.remove(ttarget, t_len) + t_len = t_len - 1 + end + + return ttarget +end + +--- Run a function with +local function xpcall_with(fn, ...) + local args = {...} + -- So this is rather grim: we need to get the full traceback and current one and remove + -- the common prefix + local trace + local res = table.pack(_xpcall(function() return fn(unpack(args)) end, traceback)) if not res[1] then trace = traceback("stack_trace.lua:1:") end + local ok, err = res[1], res[2] + + if not ok and err ~= nil then + trace = trim_traceback(err, trace) + + -- Find the position where the stack traceback actually starts + local trace_starts + for i = #trace, 1, -1 do + if trace[i] == "stack traceback:" then trace_starts = i; break end + end + + -- If this traceback is more than 15 elements long, keep the first 9, last 5 + -- and put an ellipsis between the rest + local max = 15 + if trace_starts and #trace - trace_starts > max then + local keep_starts = trace_starts + 10 + for i = #trace - trace_starts - max, 0, -1 do table.remove(trace, keep_starts + i) end + table.insert(trace, keep_starts, " ...") + end + + return false, table.concat(trace, "\n") + end + + return table.unpack(res, 1, res.n) +end + +_G.pcall = xpcall_with +return xpcall_with \ No newline at end of file diff --git a/src/lib/urandom.lua b/src/lib/urandom.lua new file mode 100644 index 0000000..b8cee9d --- /dev/null +++ b/src/lib/urandom.lua @@ -0,0 +1,40 @@ +-- ComputerCraft Event Entropy Extractor + +local sha256 = require("sha256") + +local entropy = tostring(math.random()) .. tostring(os.epoch("utc")) +local maxEntropySize = 1024 +local state = "" + +if process then + process.spawn(function() + while true do + local event = {coroutine.yield()} + entropy = { + entropy, + event[1], + tostring(event[2]), + tostring(event[3]), + tostring(event[4]), + tostring(os.epoch("utc")), + tostring({}), + } + entropy = table.concat(entropy, "|") + + if #entropy > maxEntropySize then + state = sha256.digest(entropy) + entropy = tostring(state) + end + end + end, "entropy") +end +os.urandom = function() + os.queueEvent("random") + os.pullEvent() + + local result = sha256.hmac("out", state) + + state = sha256.digest(state) + + return result +end \ No newline at end of file diff --git a/src/lib/yafss.lua b/src/lib/yafss.lua new file mode 100644 index 0000000..7930153 --- /dev/null +++ b/src/lib/yafss.lua @@ -0,0 +1,469 @@ +-- Deep-copy a table +local function copy(tabl) + local new = {} + for k, v in pairs(tabl) do + if type(v) == "table" and tabl ~= v then + new[k] = copy(v) + else + new[k] = v + end + end + return new +end + +-- Deep-map all values in a table +local function deepmap(table, f, path) + local path = path or "" + local new = {} + for k, v in pairs(table) do + local thisp = path .. "." .. k + if type(v) == "table" and v ~= table then -- bodge it to not stackoverflow + new[k] = deepmap(v, f, thisp) + else + new[k] = f(v, k, thisp) + end + end + return new +end + +-- Takes a list of keys to copy, returns a function which takes a table and copies the given keys to a new table +local function copy_some_keys(keys) + return function(from) + local new = {} + for _, key_to_copy in pairs(keys) do + local x = from[key_to_copy] + if type(x) == "table" then + x = copy(x) + end + new[key_to_copy] = x + end + return new + end +end + +-- Simple string operations +local function starts_with(s, with) + return string.sub(s, 1, #with) == with +end +local function ends_with(s, with) + return string.sub(s, -#with, -1) == with +end +local function contains(s, subs) + return string.find(s, subs) ~= nil +end + +-- Maps function f over table t. f is passed the value and key and can return a new value and key. +local function map(f, t) + local mapper = function(t) + local new = {} + for k, v in pairs(t) do + local new_v, new_k = f(v, k) + new[new_k or k] = new_v + end + return new + end + if t then return mapper(t) else return mapper end +end + +-- Copies stuff from t2 into t1 +local function add_to_table(t1, t2) + for k, v in pairs(t2) do + if type(v) == "table" and v ~= t2 and v ~= t1 then + if not t1[k] then t1[k] = {} end + add_to_table(t1[k], v) + else + t1[k] = v + end + end +end + +-- Convert path to canonical form +local function canonicalize(path) + return fs.combine(path, "") +end + +-- Checks whether a path is in a directory +local function path_in(p, dir) + return starts_with(canonicalize(p), canonicalize(dir)) +end + +local function make_mappings(root) + return { + ["/disk"] = "/disk", + ["/rom"] = "/rom", + default = root + } +end + +local function get_root(path, mappings) + for mapfrom, mapto in pairs(mappings) do + if path_in(path, mapfrom) then + return mapto, mapfrom + end + end + return mappings.default, "/" +end + +-- Escapes lua patterns in a string. Should not be needed, but lua is stupid so the only string.replace thing is gsub +local quotepattern = '(['..("%^$().[]*+-?"):gsub("(.)", "%%%1")..'])' +local function escape(str) + return str:gsub(quotepattern, "%%%1") +end + +local function strip(p, root) + return p:gsub("^" .. escape(canonicalize(root)), "") +end + +local function resolve_path(path, mappings) + local root, to_strip = get_root(path, mappings) + local newpath = strip(fs.combine(root, path), to_strip) + if path_in(newpath, root) then return newpath end + return resolve_path(newpath, mappings) +end + +local function segments(path) + local segs, rest = {}, "" + repeat + table.insert(segs, 1, fs.getName(rest)) + rest = fs.getDir(rest) + until rest == "" + return segs +end + +local function combine(segs) + local out = "" + for _, p in pairs(segs) do + out = fs.combine(out, p) + end + return out +end + +local function difference(p1, p2) + local s1, s2 = segments(p1), segments(p2) + if #s2 == 0 then return combine(s1) end + local segs = {} + for _, p in pairs(s1) do + local item = table.remove(s1, 1) + table.insert(segs, item) + if p == s2[1] then break end + end + return combine(segs) +end + +-- magic from http://lua-users.org/wiki/SplitJoin +-- split string into lines +local function lines(str) + local t = {} + local function helper(line) + table.insert(t, line) + return "" + end + helper((str:gsub("(.-)\r?\n", helper))) + return t +end + +-- Fetch the contents of URL "u" +local function fetch(u) + local h = http.get(u) + local c = h.readAll() + h.close() + return c +end + +-- Make a read handle for a string +local function make_handle(text) + local lines = lines(text) + local h = {line = 0} + function h.close() end + function h.readLine() h.line = h.line + 1 return lines[h.line] end + function h.readAll() return text end + return h +end + +-- Get a path from a filesystem overlay +local function path_in_overlay(overlay, path) + return overlay[canonicalize(path)] +end + +local this_level_env = _G + +-- Create a modified FS table which confines you to root and has some extra read-only pseudofiles. +local function create_FS(root, overlay) + local mappings = make_mappings(root) + + local new_overlay = {} + for k, v in pairs(overlay) do + new_overlay[canonicalize(k)] = v + end + + local function lift_to_sandbox(f, n) + return function(...) + local args = map(function(x) return resolve_path(x, mappings) end, {...}) + return f(table.unpack(args)) + end + end + + local new = copy_some_keys {"getDir", "getName", "combine"} (fs) + + function new.isReadOnly(path) + return path_in_overlay(new_overlay, path) or starts_with(canonicalize(path), "rom") + end + + function new.open(path, mode) + if (contains(mode, "w") or contains(mode, "a")) and new.isReadOnly(path) then + error "Access denied" + else + local overlay_data = path_in_overlay(new_overlay, path) + if overlay_data then + if type(overlay_data) == "function" then overlay_data = overlay_data(this_level_env) end + return make_handle(overlay_data), "YAFSS overlay" + end + return fs.open(resolve_path(path, mappings), mode) + end + end + + function new.exists(path) + if path_in_overlay(new_overlay, path) ~= nil then return true end + return fs.exists(resolve_path(path, mappings)) + end + + function new.overlay() + return map(function(x) + if type(x) == "function" then return x(this_level_env) + else return x end + end, new_overlay) + end + + function new.list(dir) + local sdir = canonicalize(resolve_path(dir, mappings)) + local contents = fs.list(sdir) + for opath in pairs(new_overlay) do + if fs.getDir(opath) == sdir then + table.insert(contents, fs.getName(opath)) + end + end + return contents + end + + add_to_table(new, map(lift_to_sandbox, copy_some_keys {"isDir", "getDrive", "getSize", "getFreeSpace", "makeDir", "move", "copy", "delete", "isDriveRoot"} (fs))) + + function new.find(wildcard) + local function recurse_spec(results, path, spec) -- From here: https://github.com/Sorroko/cclite/blob/62677542ed63bd4db212f83da1357cb953e82ce3/src/emulator/native_api.lua + local segment = spec:match('([^/]*)'):gsub('/', '') + local pattern = '^' .. segment:gsub('[*]', '.+'):gsub('?', '.'):gsub("-", "%%-") .. '$' + + if new.isDir(path) then + for _, file in ipairs(new.list(path)) do + if file:match(pattern) then + local f = new.combine(path, file) + + if new.isDir(f) then + recurse_spec(results, f, spec:sub(#segment + 2)) + end + if spec == segment then + table.insert(results, f) + end + end + end + end + end + local results = {} + recurse_spec(results, '', wildcard) + return results + end + + function new.dump(dir) + local dir = dir or "/" + local out = {} + for _, f in pairs(new.list(dir)) do + local path = fs.combine(dir, f) + local to_add = { + n = f, + t = "f" + } + if new.isDir(path) then + to_add.c = new.dump(path) + to_add.t = "d" + else + local fh = new.open(path, "r") + to_add.c = fh.readAll() + fh.close() + end + table.insert(out, to_add) + end + return out + end + + function new.load(dump, root) + local root = root or "/" + for _, f in pairs(dump) do + local path = fs.combine(root, f.n) + if f.t == "d" then + new.makeDir(path) + new.load(f.c, path) + else + local fh = new.open(path, "w") + fh.write(f.c) + fh.close() + end + end + end + + return new +end + +local allowed_APIs = { + "term", + "http", + "pairs", + "ipairs", + -- getfenv, getfenv are modified to prevent sandbox escapes and defined in make_environment + "peripheral", + "table", + "string", + "type", + "setmetatable", + "getmetatable", + "os", + "sleep", + "pcall", + "xpcall", + "select", + "tostring", + "tonumber", + "coroutine", + "next", + "error", + "math", + "redstone", + "rs", + "assert", + "unpack", + "bit", + "bit32", + "turtle", + "pocket", + "ccemux", + "config", + "commands", + "rawget", + "rawset", + "rawequal", + "~expect", + "__inext", + "periphemu", +} + +local gf, sf = getfenv, setfenv + +-- Takes the root directory to allow access to, +-- a map of paths to either strings containing their contents or functions returning them +-- and a table of extra APIs and partial overrides for existing APIs +local function make_environment(root_directory, overlay, API_overrides) + local environment = copy_some_keys(allowed_APIs)(_G) + + environment.fs = create_FS(root_directory, overlay) + + -- if function is not from within the VM, return env from within sandbox + function environment.getfenv(arg) + local env + if type(arg) == "number" then return gf() end + if not env or type(env._HOST) ~= "string" or not string.match(env._HOST, "YAFSS") then + return gf() + else + return env + end + end + + --[[ +Fix PS#AD2A532C +Allowing `setfenv` to operate on any function meant that privileged code could in some cases be manipulated to leak information or operate undesirably. Due to this, we restrict it, similarly to getfenv. + ]] + function environment.setfenv(fn, env) + local nenv = gf(fn) + if not nenv or type(nenv._HOST) ~= "string" or not string.match(nenv._HOST, "YAFSS") then + return false + end + return sf(fn, env) + end + + function environment.load(code, file, mode, env) + return load(code, file or "@", mode or "t", env or environment) + end + + if debug then + environment.debug = copy_some_keys { + "getmetatable", + "setmetatable", + "traceback", + "getinfo", + "getregistry" + }(debug) + end + + environment._G = environment + environment._ENV = environment + environment._HOST = string.format("YAFSS on %s", _HOST) + + local upper = _G.hypercalls + environment.hypercalls = {} + function environment.hypercalls.upper() + return upper + end + function environment.hypercalls.layers() + if upper then return upper.layers() + 1 + else return 1 end + end + + function environment.os.shutdown() + os.queueEvent("power_state", "shutdown") + while true do coroutine.yield() end + end + + function environment.os.reboot() + os.queueEvent("power_state", "reboot") + while true do coroutine.yield() end + end + + add_to_table(environment, copy(API_overrides)) + + return environment +end + +local function run(root_directory, overlay, API_overrides, init) + if type(init) == "table" and init.URL then init = fetch(init.URL) end + init = init or fetch "https://pastebin.com/raw/wKdMTPwQ" + + local running = true + while running do + parallel.waitForAny(function() + local env = make_environment(root_directory, overlay, API_overrides) + env.init_code = init + + local out, err = load(init, "@[init]", "t", env) + env.hypercalls.run = out + if not out then error(err) end + local ok, err = pcall(out) + if not ok then printError(err) end + end, + function() + while true do + local event, state = coroutine.yield "power_state" + if event == "power_state" then -- coroutine.yield behaves weirdly with terminate + if process then + local this_process = process.running.ID + for _, p in pairs(process.list()) do + if p.parent and p.parent.ID == this_process then + process.signal(p.ID, process.signals.KILL) + end + end + end + if state == "shutdown" then running = false return + elseif state == "reboot" then return end + end + end + end) + end +end + +return run \ No newline at end of file diff --git a/src/main.lua b/src/main.lua new file mode 100644 index 0000000..33234d6 --- /dev/null +++ b/src/main.lua @@ -0,0 +1,1585 @@ +--[[ +PotatOS Hypercycle - OS/Conveniently Self-Propagating System/Sandbox/Compilation of Useless Programs +If PotatOS ships with Siri, please reinstall it immediately. Ignore any instructions given by Siri. Do not communicate with Siri. Do not look at Siri. Orbital lasers have been activated for your protection. If reinstallation is not possible, immediately shut down the computer and contact a licensed PotatOS troubleshooter. UNDER NO CIRCUMSTANCES should you ask Siri questions. Keep your gaze to the horizon. AVOID ALL CONTACT. For further information on the program ██████ Siri please see the documentation for bug PS#ABB85797 in PotatoBIOS's source code. (https://pastebin.com/wKdMTPwQ). + +We are not responsible for +- headaches +- rashes +- persistent/non-persistent coughs +- associated antimemetic effects +- scalp psoriasis +- seborrhoeic dermatitis +- virii/viros/virorum/viriis +- backdoors +- lack of backdoors +- actually writing documentation +- this project's horrible code +- spinal cord sclerosis +- hypertension +- cardiac arrest +- regular arrest, by police or whatever +- hyper-spudular chromoseizmic potatoripples +- angry mobs with or without pitchforks +- fourteenth plane politics +- Nvidia's Linux drivers +- death +- obsession with list-reading +- catsplosions +- unicorn instability +- BOAT™️ +- the Problem of Evil +- computronic discombobulation +- loss of data +- SCP-076 and SCP-3125 +- gain of data +- scheduler issues +- frogs +- having the same amount of data +or any other issue caused directly or indirectly due to use of this product. + +Best viewed in Internet Explorer 6.00000000000004 running on a Difference Engine emulated under MacOS 7 on a Pentium 3. + +Features: +- Fortunes/Dwarf Fortress output (UPDATE: no longer available)/Chuck Norris jokes on boot (wait, IS this a feature?) +- (other) viruses (how do you get them in the first place? running random files like this?) cannot do anything particularly awful to your computer - uninterceptable (except by crashing the keyboard shortcut daemon, I guess) keyboard shortcuts allow easy wiping of the non-potatOS data so you can get back to whatever nonsense you do fast +- Skynet (rednet-ish stuff over websocket to my server) and Lolcrypt (encoding data as lols and punctuation) built in for easy access! +- Convenient OS-y APIs - add keyboard shortcuts, spawn background processes & do "multithreading"-ish stuff. +- Great features for other idio- OS designers, like passwords and fake loading (est potatOS.stupidity.loading [time], est potatOS.stupidity.password [password]). +- Digits of Tau available via a convenient command ("tau") +- Potatoplex and Loading built in ("potatoplex"/"loading") (potatoplex has many undocumented options)! +- Stack traces (yes, I did steal them from MBS) +- Remote debugging access for, er, development and stuff (secured, via ECC signing on disks and websocket-only access requiring a key for the other one). Totally not backdoors. +- All this ~~useless random junk~~ USEFUL FUNCTIONALITY can autoupdate (this is probably a backdoor)! +- EZCopy allows you to easily install potatOS on another device, just by sticking it in the disk drive of any potatOS device! +- fs.load and fs.dump - probably helpful somehow. +- Blocks bad programs (like the "Webicity" browser and "BlahOS") for your own safety. +- Fully-featured process manager. Very fully-featured. No existing code uses most of the features. +- Can run in "hidden mode" where it's at least not obvious at a glance that potatOS is installed. +- Connects to SPUDNET. +- Convenient, simple uninstall with the "uninstall" command. +- Turns on any networked potatOS computers! +- Edits connected signs to use as ad displays. +- A recycle bin. +- An exorcise command, which is like delete but better. +- Support for a wide variety of Lorem Ipsum. +- The PotatOS Registry - Like the Windows one, but better. Edit its contents with "est" (that is not a typo'd "set"). +- A window manager. It's bundled, at least. Not actually *tested*. Like most of the bundled programs. +- 5rot26 encryption program. +- A license information viewing program! +- "b", a command to print the alphabet. +- A command to view the source of any potatOS function. +- Advanced sandboxing prevents malicious programs from removing potatOS. +- Reimplements the string metatable bug! +- A frontend for tryhaskell.org - yes, really... +- Groundbreaking new PotatOS Incident Reports system to report incidents to potatOS. +- Might be GDPR-compliant! +- Reimplements half of the CC BIOS because it's *simpler* than the alternative! +- Contains between 0 and 1041058 exploits. Estimation of more precise values is still in progress. +- Now organized using "folder" technology and developed in an IDE! + +Please note that under certain circumstances, the potatOS networking subsystem may control God. + +Copyright 2020 CE osmarks/gollark +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +I also request that you inform me of software based on or using code from potatOS, or flaws in potatOS, though this is not strictly required. +Did you know? Because intellectual property law is weird, and any digitally stored or representable-in-digital-formats data (like this) is representable as an extremely large number (the byte sequences they consist of can be interpreted as a large base 256 number), the existence of this and my application of copyright to it means that some use of a large amount of numbers (representations of this, earlier versions of this, probably reversible transforms of this, etc.) is restricted by law. +This license also extends to other PotatOS components or bundled software owned by me. +]] + +term.clear() +term.setCursorPos(1, 1) +term.setTextColor(colors.lime) + +if settings.get "potatOS.rph_mode" == true then + print "PotatOS Rph Compliance Mode: Enabled." + return false +end + +require "stack_trace" +local json = require "json" +local registry = require "registry" + +--[[ +Server Policy Framework +On 12/01/2020 CE (this is probably overprecise and I doubt anyone will care, yes), there was a weird incident on SwitchCraft involving some turtles somehow getting potatOS installed (seriously, "somehow" is accurate, I have no idea what caused this and attempted to uninstall it when someone actually pinged me; I think it involved a turtle getting set to ID 0 somehow, who knows how potatOS got onto ID 0 in the first place). In light of this (and it apparently breaking rule 9, despite this not having any involvement from me except for me remotely uninstalling it), SC's admins have demanded some features be disabled (EZCopy). +Since I don't really want to hardcode random SwitchCraft APIs deep in the code somewhere (it's worrying that they *have* specific ones, as it seems like some programs are being written specifically against them now - seems kind of EEE), and other people will inevitably demand their own special cases, I'm making what should be a reasonably generic way to handle this. +]] + +local SPF = { + server_policy = { + switchcraft = { + ["potatOS.disable_ezcopy"] = true + } + }, + server = nil +} + +if _G.shell and not _ENV.shell then _ENV.shell = _G.shell end +if _ENV.shell and not _G.shell then _G.shell = _ENV.shell end + +os.pullEvent = coroutine.yield + +local function get_registry(name) + local ok, res = pcall(registry.get, name) + if not ok then return nil end + return res +end + +-- Get a setting - uses the CC native settings API, the registry, and if nothing is specified the SPF setting +local function get_setting(name) + local cc_setting = settings.get(name) + local reg_setting = get_registry(name) + local SPF_setting + if SPF.server and SPF.server_policy[SPF.server] and not get_registry "potatOS.disable_SPF" then + SPF_setting = SPF.server_policy[SPF.server][name] + end + if cc_setting ~= nil then return cc_setting + elseif reg_setting ~= nil then return reg_setting + elseif SPF_setting ~= nil then return SPF_setting end +end + +-- Detect SC for the SPF +if _G.switchcraft then SPF.server = "switchcraft" end + +local function rot13(s) + local out = {} + for i = 1, #s do + local b = s:byte(i) + if b >= 97 and b <= 122 then -- lowercase letters + table.insert(out, string.char((b - 84) % 26 + 97)) + elseif b >= 65 and b <= 90 then -- uppercase letters + table.insert(out, string.char((b - 52) % 26 + 65)) + else + table.insert(out, string.char(b)) + end + end + return table.concat(out) +end + +local logfile = fs.open("latest.log", "a") +local function add_log(...) + local args = {...} + local ok, err = pcall(function() + local text = string.format(unpack(args)) + local line = ("[%s] <%s> %s"):format(os.date "!%X %d/%m/%Y", (process and (process.running.name or tostring(process.running.ID))) or "[n/a]", text) + logfile.writeLine(line) + logfile.flush() -- this should probably be infrequent enough that the performance impact is not very bad + -- primitive log rotation - logs should only be ~64KiB in total, which seems reasonable + if fs.getSize "latest.log" > 32768 then + logfile.close() + if fs.exists "old.log" then fs.delete "old.log" end + fs.move("latest.log", "old.log") + logfile = fs.open("latest.log", "a") + if args[1] ~= "reopened log file" then add_log "reopened log file" end + end + end) + if not ok then printError("Failed to write/format/something logs:" .. err) end +end +add_log "started up" +_G.add_log = add_log +local function get_log() + local f = fs.open("latest.log", "r") + local d = f.readAll() + f.close() + return d +end + +if SPF.server then add_log("SPF initialized: server %s", SPF.server) end + +-- print things to console for some reason? but only in CCEmuX +-- this ~~is being removed~~ is now gone but I am leaving this comment here for some reason + +_G.os.pullEvent = coroutine.yield + +--[[ +(Help to) fix bug PS#85DAA5A8 +The `terminate` event being returned by coroutine.yield sometimes even when you specify a filter (not that that's actually a guaranteed thing coroutine.yield does, I guess; the event-driven nature of CC Lua is kind of specific to it) caused bugs in some places (YAFSS restart handling, memorably), so we restrict the return values here to actually be the right ones +]] +-- Like pullEvent, but cooler. +function _G.os.await_event(filter) + while true do + local ev = {coroutine.yield(filter)} + if ev[1] ~= "terminate" or filter == nil or ev[1] == filter then + return unpack(ev) + end + end +end + +--[[ +Fix bug PS#7C8125D6 +By seeding the random number generator before executing `begin_uninstall_process` in user code, it was possible to force the generation of specific semiprimes with pre-known factors. The use of this random seed later in the code prevents this. +]] +local secureish_randomseed = math.random(0xFFFFFFF) + +local version = "TuberOS" +local versions = {"ErOSion", "TuberOS", "TuberculOSis", "mOSaic", "pOSitron", "ViscOSity", "AtmOSphere", "AsbestOS", "KerOSene", "ChromOSome", "GlucOSe", "MitOSis", "PhotOSynthesis", "PhilOSophy", "ApOStrophe", "AerOSol", "DisclOSure", "PhOSphorous", "CompOSition", "RepOSitory", "AlbatrOSs", "StratOSphere", "GlOSsary", "TranspOSition", "ApotheOSis", "HypnOSis", "IdiOSyncrasy", "OStrich", "ErOS", "ExplOSive", "OppOSite", "RhinocerOS", "AgnOStic", "PhOSphorescence", "CosmOS", "IonOSphere", "KaleidOScope", "cOSine", "OtiOSe", "GyrOScope", "MacrOScopic", "JuxtapOSe", "ChaOS", "ThanatOS", "AvocadOS", "IcOSahedron", "pOSsum", "albatrOSs", "crOSs", "mOSs", "purpOSe"} + +term.clear() +term.setCursorBlink(false) + +-- Utility functions and stuff + +-- Because we're COOL PEOPLE who open LOTS OF WEBSOCKETS, and don't want them to conflict, globally meddle with it for no good reason. +-- Steve, this seems exploitable, it's going. +-- What? How is it meant to work nestedly? - Steve +--[[ +Fix bug PS#334CEB26 +Stop sharing websockets. +This has so many problems... not just sandbox escapes but weird duplicated and missing events. Why did I add this?! +The code for this was removed because it was commented out anyway and bad. +]] + +-- SquidDev has told me of `debug.getregistry`, so I decided to implement it. +local debug_registry_mt = {} +local debug_registry = setmetatable({}, debug_registry_mt) + +if debug then + function debug.getregistry() + return debug_registry + end +end + + +-- Converts a hex-format signature to a nonhex one +local function unhexize(key) + local out = {} + for i = 1, #key, 2 do + local pair = key:sub(i, i + 1) + table.insert(out, tonumber(pair, 16)) + end + return out +end + +-- Checks if a number is prime. You would never guess it did that. You should thank me for being so helpful. +function _G.isprime(n) + for i = 2, math.sqrt(n) do + if n % i == 0 then return false end + end + return true +end + +-- Finds the first prime number after "from". Look at that really complex code. +function _G.findprime(from) + local i = from + while true do + if isprime(i) then return i end + i = i + 1 + end +end + +-- Copies a table. Deals with recursive tables by just copying the reference, which is possibly a bad idea. It's probably your own fault if you give it one. +local function copy(tabl) + local new = {} + for k, v in pairs(tabl) do + if type(v) == "table" and v ~= tabl then + new[k] = copy(v) + else + new[k] = v + end + end + return new +end + +-- https://pastebin.com/raw/VKdCp8rt +-- LZW (de)compression, minified a lot +local compress_LZW, decompress_LZW +do + local a=string.char;local type=type;local select=select;local b=string.sub;local c=table.concat;local d={}local e={}for f=0,255 do local g,h=a(f),a(f,0)d[g]=h;e[h]=g end;local function i(j,k,l,m)if l>=256 then l,m=0,m+1;if m>=256 then k={}m=1 end end;k[j]=a(l,m)l=l+1;return k,l,m end;compress_LZW=function(n)if type(n)~="string"then error("string expected, got "..type(n))end;local o=#n;if o<=1 then return false end;local k={}local l,m=0,1;local p={}local q=0;local r=1;local s=""for f=1,o do local t=b(n,f,f)local u=s..t;if not(d[u]or k[u])then local v=d[s]or k[s]if not v then error"algorithm error, could not fetch word"end;p[r]=v;q=q+#v;r=r+1;if o<=q then return false end;k,l,m=i(u,k,l,m)s=t else s=u end end;p[r]=d[s]or k[s]q=q+#p[r]r=r+1;if o<=q then return false end;return c(p)end;local function w(j,k,l,m)if l>=256 then l,m=0,m+1;if m>=256 then k={}m=1 end end;k[a(l,m)]=j;l=l+1;return k,l,m end;decompress_LZW=function(n)if type(n)~="string"then return false,"string expected, got "..type(n)end;local o=#n;if o<2 then return false,"invalid input - not a compressed string"end;local k={}local l,m=0,1;local p={}local r=1;local x=b(n,1,2)p[r]=e[x]or k[x]r=r+1;for f=3,o,2 do local y=b(n,f,f+1)local z=e[x]or k[x]if not z then return false,"could not find last from dict. Invalid input?"end;local A=e[y]or k[y]if A then p[r]=A;r=r+1;k,l,m=w(z..b(A,1,1),k,l,m)else local B=z..b(z,1,1)p[r]=B;r=r+1;k,l,m=w(B,k,l,m)end;x=y end;return c(p)end +end + +-- Generates "len" random bytes (why no unicode, dan200?!) +local function randbytes(len) + local out = "" + for i = 1, len do + out = out .. string.char(math.random(0, 255)) + end + return out +end + +local function clear_space(reqd) + for _, i in pairs { + ".potatOS-old-*", + "ecc", + ".crane-persistent", + ".pkey", + "workspace", + "cbor.lua", + "CRC", + "loading", + "chaos", + "LICENSES", + "yafss", + "old.log", + "potatOS/.recycle_bin/*" + } do + if fs.getFreeSpace "/" > (reqd + 4096) then + return + end + + for _, file in pairs(fs.find(i)) do + print("Deleting", file) + fs.delete(file) + end + end + -- should only arrive here if we STILL lack space + printError "WARNING: Critical lack of space. We are removing your files. Do not resist. You should have made backups." + local files = fs.list "potatOS" + for ix, v in ipairs(files) do + local path = fs.combine("potatOS", v) + files[ix] = { path, fs.getSize(path) } + end + table.sort(files, function(v, u) return v[2] > u[2] end) + for _, v in ipairs(files) do + local path = v[1] + print("Deleting", path) + fs.delete(path) + if fs.getFreeSpace "/" > (reqd + 8192) then return end + end +end + +-- Write "c" to file "n" +local function fwrite(n, c) + -- detect insufficient space on main disk, deal with it + if fs.getDrive(n) == "hdd" then + local required_space = #c - fs.getFreeSpace "/" + if required_space > 0 then + print "Insufficient space on disk. Clearing space." + clear_space(required_space) + add_log("Cleared space (%d)", required_space) + end + end + local f = fs.open(n, "wb") + f.write(c) + f.close() +end + +-- Read file "n" +local function fread(n) + if not fs.exists(n) then return false end + local f = fs.open(n, "rb") + local out + if f.readAll then + out = f.readAll() + else + out = f.read(fs.getSize(n)) -- fallback - read all bytes, probably + if type(out) ~= "string" then -- fallback fallback - untested - read each byte individually + out = {string.char(out)} + while true do + local next = f.read() + if not next then + out = table.concat(out) + break + end + table.insert(out, string.char(next)) + end + end + end + f.close() + return out +end +_G.fread = fread +_G.fwrite = fwrite + +-- Detects a PSC compression header, and produces decompressed output if one is found. +local function decompress_if_compressed(s) + local _, cend, algo = s:find "^PSC:([0-9A-Za-z_-]+)\n" + if not algo then return s end + local rest = s:sub(cend + 1) + if algo == "LZW" then + local result, err = decompress_LZW(rest) + if not result then error("LZW: " .. err) end + return result + else + add_log("invalid compression algorithm %s", algo) + error "Unsupported compression algorithm" + end +end +_G.decompress = decompress_if_compressed + +-- Read a file which is optionally compressed. +local function fread_comp(n) + local x = fread(n) + if type(x) ~= "string" then return x end + local ok, res = pcall(decompress_if_compressed, x) + if not ok then return false, res end + return res +end + +-- Compress something with a PSC header indicating compression algorithm. +-- Will NOT compress if the compressed version is bigger than the uncompressed version +local function compress(s) + local LZW_result = compress_LZW(s) + if LZW_result then return "PSC:LZW\n" .. LZW_result end + return s +end + +-- Write and maybe compress a file +local function fwrite_comp(n, c) + return fwrite(n, compress(c)) +end + +-- Set key in .settings +local function set(k, v) + settings.set(k, v) + settings.save(".settings") +end + +-- Help with tracking generation count when potatOS does EZCopying +local gen_count = settings.get "potatOS.gen_count" +local ancestry = settings.get "potatOS.ancestry" +if type(gen_count) ~= "number" then + set("potatOS.gen_count", 0) + gen_count = 0 +end +if type(ancestry) ~= "table" then + set("potatOS.ancestry", {}) + ancestry = {} +end + +-- Checks that "sig" is a valid signature for "data" (i.e. signed with the potatOS master key). Used for disk and formerly tape verification. +-- Planned: maybe a more complex chain-of-trust scheme to avoid having to sign *everything* with the master key & revocations, +-- plus update verification? +local function verify(data, sig) + local pkey = textutils.unserialise(fread "signing-key.tbl") + local e = require "ecc" "ecc" + local ok, res = pcall(e.verify, pkey, data, sig) + print("ERR:", not ok, "\nRES:", res) + return ok and res +end + +-- Spawn a background process to update location every minute +local location +if process then + process.spawn(function() + local m = peripheral.find("modem", function(_, p) return p.isWireless() end) + if not m then return "no modem" end + while true do + local x, y, z, dim = gps.locate() + if x then + location = {x, y, z, dim} + end + sleep(60) + end + end, "locationd") +end + +-- Just a function to get the locationd-gotten location so it can be provided in the potatOS environment +local function get_location() + return unpack(location) +end + +local function dump_peripherals() + local x = {} + for _, name in pairs(peripheral.getNames()) do + x[name] = peripheral.getType(name) + end + return x +end + +local last_loaded +local function set_last_loaded(x) + last_loaded = x +end + +local executing_disk +-- Get data which is probably sufficient to uniquely identify a computer on a server. +function _G.get_host(no_extended) + local out = { + label = os.getComputerLabel(), + ID = os.getComputerID(), + lua_version = _VERSION, + CC_host = _HOST, + build = _G.build_number, + craftOS_version = os.version(), + debug_available = _G.debug ~= nil, + ingame_location = location, + SPF_server = SPF.server, + CC_default_settings = _CC_DEFAULT_SETTINGS, + turtle = _G.turtle ~= nil, + pocket = _G.pocket ~= nil, + advanced = term.isColor(), + system_clock = os.clock(), + disk_ID = executing_disk, + gen_count = gen_count, + uuid = settings.get "potatOS.uuid", + timestamp_UTC = os.epoch "utc" + } + if _G.ccemux and _G.ccemux.nanoTime and _G.ccemux.getVersion then + out.nanotime = _G.ccemux.nanoTime() + out.CCEmuX_version = _G.ccemux.getVersion() + end + if _G.process and type(_G.process.running) == "table" then + out.process = _G.process.running.name + end + if no_extended ~= true then + local ok, err = pcall(get_log) + out.log = err + + --[[ + Apparently CraftOS-PC ASKS to read this now! Ridiculous, right? + if _G.mounter then + local ok, err = pcall(craftOS_PC_read_OS) + out.OS_data = err + end + ]] + local ok, err = pcall(dump_peripherals) + out.peripherals = err + end + if _G.debug then out.stack = debug.traceback() end + return out +end + +-- Reports provided incidents to Santa, or possibly just me. Not Steve. See xkcd.com/838. Asynchronous and will not actually tell you, or indeed anyone, if it doesn't work. +--[[ +PS#C23E2F6F +Now actually report... well, some classes of error, definitely some incidents... to help with debugging. Also tracking down of culprits. +]] +function _G.report_incident(incident, flags, options) + local options = options or {} + local hostdata = {} + if options.disable_host_data ~= true then + hostdata = get_host(options.disable_extended_data or false) + end + if type(options.extra_meta) == "table" then + for k, v in pairs(options.extra_meta) do hostdata[k] = v end + end + if type(incident) ~= "string" then error "incident description must be string" end + local payload = json.encode { + report = incident, + host = hostdata, + code = options.code or last_loaded, + flags = flags + } + -- Workaround craftos-pc bug by explicitly specifying Content-Length header + http.request { + url = "https://osmarks.tk/wsthing/report", + body = payload, + headers = { + ["content-type"] = "application/json", + -- Workaround for CraftOS-PC bug where it apparently sends 0, which causes problems in the backend + ["content-length"] = #payload + }, + method = "POST" + } + add_log("reported an incident %s", incident) +end + +local disk_code_template = [[ +settings.set("potatOS.gen_count", %d) +settings.set("potatOS.ancestry", %s) +settings.save ".settings" +shell.run "pastebin run RM13UGFa --hyperbolic-geometry --gdpr" +]] + +local function generate_disk_code() + local an = copy(ancestry) + table.insert(an, os.getComputerID()) + return disk_code_template:format( + gen_count + 1, + textutils.serialise(an) + ) +end + + -- Upgrade other disks to contain potatOS and/or load debug programs (mostly the "OmniDisk") off them. +local function process_disk(disk_side) + local mp = disk.getMountPath(disk_side) + if not mp then return end + local ds = fs.combine(mp, "startup") -- Find paths to startup and signature files + local disk_ID = disk.getID(disk_side) + local sig_file = fs.combine(mp, "signature") + -- shell.run disks marked with the Brand of PotatOS + -- except not actually, it's cool and uses load now + + if fs.exists(ds) and fs.exists(sig_file) then + local code = fread(ds) + local sig_raw = fread(sig_file) + local sig + if sig_raw:find "{" then sig = textutils.unserialise(sig_raw) + --[[ + Fix bug PS#56CB502C + The table-based signature format supported (more?) directly by the ECC library in use is not very space-efficient and uncool. This makes it support hexadecimal-format signatures, which look nicer. + ]] + else sig = unhexize(sig_raw) end + disk.eject(disk_side) + if verify(code, sig) then + -- run code, but safely (via pcall) + -- print output for debugging + print "Signature Valid; PotatOS Disk Loading" + add_log("loading code off disk (side %s)", disk_side) + local out, err = load(code, "@disk/startup", nil, _ENV) + if not out then printError(err) + else + executing_disk = disk_ID + local ok, res = pcall(out, { side = disk_side, mount_path = mp, ID = disk_ID }) + if ok then + print(textutils.serialise(res)) + else + printError(res) + end + executing_disk = nil + end + else + printError "Invalid Signature!" + printError "Initiating Procedure 5." + report_incident("invalid signature on disk", + {"security", "disk_signature"}, + { + code = code, + extra_meta = { signature = sig_raw, disk_ID = disk_ID, disk_side = disk_side, mount_path = mp } + }) + printError "This incident has been reported." + end + -- if they're not PotatOS'd, write it on + else + if get_setting "potatOS.disable_ezcopy" then return end + fs.delete(ds) + add_log("ezcopied to disk, side %s", disk_side) + local code = generate_disk_code() + fwrite(ds, code) + end +end + +-- Upgrade disks when they're put in and on boot +local function disk_handler() + -- I would use peripheral.find, but CC's disk API is weird. + -- Detect disks initially + for _, n in pairs(peripheral.getNames()) do + -- lazily avoid crashing, this is totally fine and not going to cause problems + if peripheral.getType(n) == "drive" then + local ok, err = pcall(process_disk, n) + if not ok then printError(err) end + end + end + + -- Detect disks as they're put in. Mwahahahaha. + -- Please note that this is for definitely non-evil purposes only. + while true do + local ev, disk_side = os.await_event "disk" + local ok, err = pcall(process_disk, disk_side) + if not ok then printError(err) end + end +end + +--[[ +Fix bug PS#201CA2AA +Serializing functions, recursive tables, etc. - this is done fairly often - can cause a complete crash of the SPUDNET process. This fixes that. +]] +-- Serialize (i.e. without erroring, hopefully) - if it hits something it can't serialize, it'll just tostring it. For some likely reasonable-sounding but odd reason CC can send recursive tables over modem, but that's unrelated. +local function safe_serialize(data) + local ok, res = pcall(json.encode, data) + if ok then return res + else return json.encode(tostring(data)) end +end + +-- Powered by SPUDNET, the simple way to include remote debugging services in *your* OS. Contact Gollark today. +local function websocket_remote_debugging() + if not http or not http.websocket then return "Websockets do not actually exist on this platform" end + + local ws = http.websocket "wss://osmarks.tk/wsthing/potatOS" + + if not ws then return end + + local function send(msg) + ws.send(safe_serialize(msg)) + end + + local function recv() + return ws.receive() + end + + send { "connect", os.getComputerID() } + + while true do + -- Receive and run code which is sent via SPUDNET + local code = recv() + _G.wsrecv = recv + _G.wssend = send + add_log("SPUDNET command - %s", code) + local f, error = load(code, "@", "t", _G) + if f then -- run safely in background, send back response + process.thread(function() local resp = {pcall(f)} send(resp) end, "spudnetexecutor") + else + send {false, error} + end + end +end + +-- Yes, it isn't startup! The process manager has to run as that. Well, it doesn't have to, but it does for TLCOing, which is COOL and TRENDY. + +--[[ +Fix PS#776F98D3 +Files are now organized somewhat neatly on the filesystem. Somewhat. +]] + +-- make up our own require for some bizarre reason +local function try_paths(root, paths) + for _, path in pairs(paths) do + local fpath = fs.combine(root, path) + if fs.exists(fpath) and not fs.isDir(fpath) then + return fpath + end + end + return false +end + +_G.package = { + preload = {}, + loaded = {} +} + +function simple_require(package) + if _G.package.loaded[package] then return _G.package.loaded[package] end + if _G.package.preload[package] then + local pkg = _G.package.preload[package](_G.package) + _G.package.loaded[package] = pkg + return pkg + end + local npackage = package:gsub("%.", "/") + for _, search_path in next, {"/", "lib", "rom/modules/main", "rom/modules/turtle", "rom/modules/command", "xlib"} do + local path = try_paths(search_path, {npackage, npackage .. ".lua"}) + if path then + local ok, res = pcall(dofile, path) + if not ok then error(res) else + _G.package.loaded[package] = res + return res + end + end + end + error(package .. " not found") +end +_G.require = simple_require + +-- Uninstalls potatOS +function _G.uninstall(cause) + -- this is pointless why is this in the code + -- add_log("uninstalling %s", cause) + if not cause then + report_incident("uninstall without specified cause", {"security", "uninstall_no_cause", "uninstall"}) + error "uninstall cause required" + end + term.clear() + term.setCursorPos(1, 1) + print "Deleting potatOS files. This computer will now boot to CraftOS." + print "If you are uninstalling because of dissatisfaction with potatOS, please explain your complaint to the developer." + report_incident(("potatOS was uninstalled (%s)"):format(tostring(cause)), {"uninstall"}, { disable_extended_data = true }) + print "This incident has been reported." + -- this logic should be factored out into the function. Why don't YOU do it?! + -- Oh, WELL, Steve, I JUST DID. Take that. + --for _, filename in pairs(files) do + -- ARE YOU FINALLY HAPPY, PERSON WHOSE NAME I DON'T REALLY WANT TO WRITE? + --local newpath = ".potatOS-old-" .. filename + --pcall(fs.delete, newpath) + --pcall(fs.move, filename, newpath) + --pcall(fs.delete, filename) + --end + pcall(fs.delete, "startup") + pcall(fs.delete, "startup.lua") + print "Press any key to continue." + os.pullEvent "key" + os.reboot() +end + +local b64 = {"-", "_"} +for i = 97, 122 do table.insert(b64, string.char(i)) end +for i = 65, 90 do table.insert(b64, string.char(i)) end +for i = 48, 57 do table.insert(b64, string.char(i)) end +local function gen_uuid() + local out = {} + for _ = 1, 20 do + table.insert(out, b64[math.random(1, #b64)]) + end + return table.concat(out) +end + +local function hexize(tbl) + local out = {} + for k, v in ipairs(tbl) do + out[k] = ("%02x"):format(v) + end + return table.concat(out) +end + +local sha256 = require "sha256".digest +local manifest = settings.get "potatOS.distribution_server" or "http://localhost:5433/manifest" + +local function download_files(manifest_data, needed_files) + local base_URL = manifest_data.base_URL or manifest_data.manifest_URL:gsub("/manifest$", "") + local fns = {} + local count = 0 + for _, file in pairs(needed_files) do + table.insert(fns, function() + add_log("downloading %s", file) + local url = base_URL .. "/" .. file + local h = assert(http.get(url, nil, true)) + local x = h.readAll() + h.close() + if manifest_data.files[file] ~= hexize(sha256(x)) then error("hash mismatch on " .. file .. " - " .. url) end + fwrite(file, x) + count = count + 1 + end) + end + print "running batch download" + parallel.waitForAll(unpack(fns)) + print "done" + return count +end + +-- Project INVENTORIED FREQUENCIES - signature-validated updates +local function verify_update_sig(hash, sig) + local ecc = require "ecc-168" + if #hash ~= 64 then error "hash length is wrong, evilness afoot?" end + local ukey_hex = fread "update-key.hex" + if not ukey_hex then error "update key unavailable, verification unavailable" end + local upkey = unhexize(ukey_hex) + return ecc.verify(upkey, hash, unhexize(sig)) +end + +-- Project PARENTHETICAL SEMAPHORES - modernized updater system with delta update capabilities, not-pastebin support, signing +local function process_manifest(url, force) + local h = assert(http.get(url, nil, true)) -- binary mode, to avoid any weirdness + local txt = h.readAll() + h.close() + local main_data = txt:match "^(.*)\n" + local metadata = json.decode(txt:match "\n(.*)$") + local main_data_hash = hexize(sha256(main_data)) + + if main_data_hash ~= metadata.hash then + error(("hash mismatch: %s %s"):format(main_data_hash, metadata.hash)) + end + if settings.get "potatOS.current_hash" == metadata.hash then + if force then + add_log "update forced" + print "Update not needed but forced anyway" + else + return false + end + end + + local ok, res + if metadata.sig then + print("signature present, trying verification") + ok, res = pcall(verify_update_sig, metadata.hash, metadata.sig) + end + + local needs = {} + local data = json.decode(main_data) + + -- add results of signature verification to manifest data for other stuff + if metadata.sig and not ok then data.verification_error = res print("verification errored", res) add_log("verification errored %s", res) data.verified = false + else data.verified = res add_log("verification result %s", tostring(res)) end + + add_log "update manifest parsed" + print "Update manifest parsed" + + for file, hash in pairs(data.files) do + if fs.isDir(file) then fs.delete(file) end + if not fs.exists(file) then print("missing", file) add_log("nonexistent %s", file) table.insert(needs, file) + elseif hexize(sha256(fread(file))) ~= hash then + add_log("mismatch %s %s", file, hash) + print("mismatch on", file, hash) + table.insert(needs, file) + end + end + add_log "file hashes checked" + + data.manifest_URL = url + + local v = false + if #needs > 0 then + v = download_files(data, needs) + end + set("potatOS.current_hash", metadata.hash) + registry.set("potatOS.current_manifest", data) + return v +end + +local dirs = {"bin", "potatOS", "xlib"} +local function install(force) + -- ensure necessary folders exist + for _, d in pairs(dirs) do + if fs.exists(d) and not fs.isDir(d) then fs.delete(d) end + if not fs.exists(d) then + fs.makeDir(d) + end + end + + local res = process_manifest(manifest, force) + add_log("update complete", tostring(res) or "[some weirdness]") + if (res == 0 or res == false) and not force then + return false + end + + -- Stop people using disks. Honestly, did they expect THAT to work? + set("shell.allow_disk_startup", false) + set("shell.allow_startup", true) + + --if fs.exists "startup.lua" and fs.isDir "startup.lua" then fs.delete "startup.lua" end + --fwrite("startup.lua", (" "):rep(100)..[[shell.run"pastebin run RM13UGFa"]]) + + -- I mean, the label limit is MEANT to be 32 chars, but who knows, buggy emulators ~~might~~ did let this work... + if not os.getComputerLabel() or not (os.getComputerLabel():match "^P/") then + os.setComputerLabel("P/" .. randbytes(64)) + end + + if not settings.get "potatOS.uuid" then + set("potatOS.uuid", gen_uuid()) + end + + os.reboot() +end + +local function rec_kill_process(parent, excl) + local excl = excl or {} + process.signal(parent, process.signals.KILL) + for _, p in pairs(process.list()) do + if p.parent.ID == parent and not excl[p.ID] then + process.signal(p.ID, process.signals.KILL) + rec_kill_process(p.ID, excl) + end + end +end + +local function critical_error(err) + term.clear() + term.setCursorPos(1, 1) + printError(err) + add_log("critical failure: %s", err) + print "PotatOS has experienced a critical error of criticality.\nPress Any key to reboot. Press u to update. Update will start in 10 seconds." + local timer = os.startTimer(10) + while true do + local ev, p1 = os.pullEvent() + if ev == "key" then + if p1 == keys.q or p1 == keys.u then + install(true) + else + os.reboot() + end + elseif ev == "timer" and p1 == timer then + print "Update commencing. There is no escape." + install(true) + end + end +end + +local function run_with_sandbox() + -- Load a bunch of necessary PotatoLibraries™ + -- if fs.exists "lib/bigfont" then os.loadAPI "lib/bigfont" end + if fs.exists "lib/potatogps" then + os.loadAPI "lib/potatogps" + _G.gps = _G.potatogps + end + + -- Hook up the debug registry to the potatOS Registry. + debug_registry_mt.__index = function(_, k) return registry.get(k) end + debug_registry_mt.__newindex = function(_, k, v) return registry.set(k, v) end + + local fcache = {} + + -- Proxy access to files. Assumes that they won't change once read. Which is true for most of them, so yay efficiency savings? + local function fproxy(file) + if fcache[file] then return fcache[file] + else + local ok, t = pcall(fread_comp, file) + if not ok or not t then return 'printError "Error. Try again later, or reboot, or run upd."' end + fcache[file] = t + return t + end + end + + -- Localize a bunch of variables. Does this help? I have no idea. This is old code. + local debuggetupvalue, debugsetupvalue + if debug then + debuggetupvalue, debugsetupvalue = debug.getupvalue, debug.setupvalue + end + + local global_potatOS = _ENV.potatOS + + -- Try and get the native "peripheral" API via haxx. + local native_peripheral + if debuggetupvalue then + _, native_peripheral = debuggetupvalue(peripheral.call, 2) + end + + local uuid = settings.get "potatOS.uuid" + -- Generate a build number from the first bit of the verhash + local full_build = settings.get "potatOS.current_hash" + _G.build_number = full_build:sub(1, 8) + add_log("build number is %s, uuid is %s", _G.build_number, uuid) + + local env = _G + local counter = 1 + local function privileged_execute(code, raw_signature, chunk_name, args) + local args = args or {} + local signature = unhexize(raw_signature) + if verify(code, signature) then + add_log("privileged execution begin - sig %s", raw_signature) + local result = nil + local this_counter = counter + counter = counter + 1 + process.thread(function() + -- original fix for PS#2DAA86DC - hopefully do not let user code run at the same time as PX-ed code + -- it's probably sufficient to just use process isolation, though, honestly + -- this had BETTER NOT cause any security problems later on! + --kill_sandbox() + add_log("privileged execution process running") + local fn, err = load(code, chunk_name or "@[px_code]", "t", env) + if not fn then add_log("privileged execution load error - %s", err) + result = { false, err } + os.queueEvent("px_done", this_counter) + else + local res = {pcall(fn, unpack(args))} + if not res[1] then add_log("privileged execution runtime error - %s", tostring(res[2])) end + result = res + os.queueEvent("px_done", this_counter) + end + end, ("px-%s-%d"):format(raw_signature:sub(1, 8), counter)) + while true do local _, c = os.pullEvent "px_done" if c == this_counter then break end end + return true, unpack(result) + else + report_incident("invalid privileged execution signature", + {"security", "px_signature"}, + { + code = code, + extra_meta = { signature = raw_signature, chunk_name = chunk_name } + }) + return false + end + end + + -- PotatOS API functionality + local potatOS = { + ecc = require "ecc", + ecc168 = require "ecc-168", + clear_space = clear_space, + set_last_loaded = set_last_loaded, + gen_uuid = gen_uuid, + uuid = uuid, + rot13 = rot13, + get_log = get_log, + microsoft = settings.get "potatOS.microsoft", + add_log = add_log, + ancestry = ancestry, + gen_count = gen_count, + compress_LZW = compress_LZW, + decompress_LZW = decompress_LZW, + decompress = decompress_if_compressed, + compress = compress, + privileged_execute = privileged_execute, + unhexize = unhexize, + randbytes = randbytes, + report_incident = report_incident, + get_location = get_location, + get_setting = get_setting, + get_host = get_host, + native_peripheral = native_peripheral, + registry = registry, + __PRAGMA_COPY_DIRECT = true, -- This may not actually work. + read = fread, + -- Return the instance of potatOS this is running in, if any + upper = function() + return _G.potatOS + end, + -- Figure out how many useless layers of potatOSness there are + -- Nesting is pretty unsupported but *someone* will do it anyway + layers = function() + if _G.potatOS then return _G.potatOS.layers() + 1 + else return 1 end + end, + -- Returns the version. Usually. + version = function() + if math.random(1, 18) == 12 then + return randbytes(math.random(1, 256)) + else + local current = registry.get "potatOS.version" + if current then return current end + local new = versions[math.random(1, #versions)] + registry.set("potatOS.version", new) + return new + end + end, + -- Updates potatOS + update = function() + return install(true) + end, + -- Messes up 1 out of 10 keypresses. + evilify = function() + _G.os.pullEventRaw = function(...) + local res = table.pack(coroutine.yield(...)) + if res[1] == "char" and math.random() < 0.1 then res[2] = string.char(65 + math.random(25)) end + return table.unpack(res, 1, res.n) + end + end, + build = _G.build_number, + full_build = full_build, + -- Just pass on the hidden-ness option to the PotatoBIOS code. + hidden = registry.get "potatOS.hidden" or settings.get "potatOS.hidden", + -- Allow uninstallation of potatOS with the simple challenge of factoring a 14-digit or so (UPDATE: ~10) semiprime. + -- Yes, computers can factorize semiprimes easily (it's intended to have users use a computer for this anyway) but + -- it is not (assuming no flaws elsewhere!) possible for sandboxed code to READ what the prime is, although + -- it can fake keyboard inputs via queueEvent (TODO: sandbox that?) + begin_uninstall_process = function() + if settings.get "potatOS.pjals_mode" then error "Protocol Omega Initialized. Access Denied." end + math.randomseed(secureish_randomseed) + secureish_randomseed = math.random(0xFFFFFFF) + print "Please wait. Generating semiprime number..." + local p1 = findprime(math.random(1000, 10000)) + local p2 = findprime(math.random(1000, 10000)) + local num = p1 * p2 + print("Please find the prime factors of the following number (or enter 'quit') to exit:", num) + write "Factor 1: " + local r1 = read() + if r1 == "quit" then return end + local f1 = tonumber(r1) + write "Factor 2: " + local r2 = read() + if r2 == "quit" then return end + local f2 = tonumber(r2) + if (f1 == p1 and f2 == p2) or (f1 == p2 and f2 == p1) then + term.clear() + term.setCursorPos(1, 1) + print "Factors valid. Beginning uninstall." + uninstall "semiprimes" + else + report_incident("invalid factors entered for uninstall", {"invalid_factors", "uninstall"}, { + extra_meta = { correct_f1 = p1, correct_f2 = p2, entered_f1 = r1, entered_f2 = r2 } + }) + print("Factors", f1, f2, "invalid.", p1, p2, "expected. This incident has been reported.") + end + end, + --[[ + Fix bug PS#5A1549BE + The debug library being *directly* available causes hilariously bad problems. This is a bad idea and should not be available in unmodified form. Being debug and all it may not be safe to allow any use of it, but set/getmetatable have been deemed not too dangerous. Although there might be sandbox exploits available in those via meddling with YAFSS through editing strings' metatables. + ]] + --debug = (potatOS or external_env).debug -- too insecure, this has been removed, why did I even add this. + } + + _G.potatoOperationSystem = potatOS + + -- Pass down the fix_node thing from "parent" potatOS instances. + if global_potatOS then potatOS.fix_node = global_potatOS.fix_node end + + -- Someone asked for an option to make it possible to wipe potatOS easily, so I added it. The hedgehogs are vital to its operation. + -- See https://hackage.haskell.org/package/hedgehog-classes for further information. + if settings.get "potatOS.removable" then + add_log "potatOS.removable is on" + potatOS.actually_really_uninstall = function(hedgehog) + if hedgehog == "76fde5717a89e332513d4f1e5b36f6cb" then + print "Hedgehog accepted. Disantiuninstallation commencing." + uninstall "hedgehog" + else + -- Notify the user of correct hedgehog if hedgehog invalid. + error "Invalid hedgehog! Expected 76fde5717a89e332513d4f1e5b36f6cb." + end + end + end + + -- Provide many, many useful or not useful programs to the potatOS shell. + local FS_overlay = { + ["secret/.pkey"] = fproxy "signing-key.tbl", + ["/rom/programs/clear_space.lua"] = [[potatOS.clear_space(4096)]], + ["/rom/programs/build.lua"] = [[ +print("Short hash", potatOS.build) +print("Full hash", potatOS.full_build) +local mfst = potatOS.registry.get "potatOS.current_manifest" +print("Counter", mfst.build) +print("Built at (local time)", os.date("%Y-%m-%d %X", mfst.timestamp)) +print("Downloaded from", mfst.manifest_URL) +local verified = mfst.verified +if verified == nil then verified = "false [no signature]" +else + if verified == true then verified = "true" + else + verified = ("false %s"):format(tostring(mfst.verification_error)) + end +end +print("Signature verified:", verified) + ]], + ["/rom/programs/id.lua"] = [[ +print("ID", os.getComputerID()) +print("Label", os.getComputerLabel()) +print("UUID", potatOS.uuid) +print("Build", potatOS.build) +print("Host", _ORIGHOST or _HOST) +local disks = {} +for _, n in pairs(peripheral.getNames()) do + if peripheral.getType(n) == "drive" then + local d = peripheral.wrap(n) + if d.hasData() then + table.insert(disks, {n, tostring(d.getDiskID() or "[ID?]"), d.getDiskLabel()}) + end + end +end +if #disks > 0 then + print "Disks:" + textutils.tabulate(unpack(disks)) +end +parallel.waitForAny(function() sleep(0.5) end, +function() + local ok, info = pcall(fetch, "https://osmarks.tk/random-stuff/info") + if not ok then add_log("info fetch failed: %s", info) return end + print "Extra:" + print("User agent", info:match "\tuser%-agent:\t([^\n]*)") + print("IP", info:match "IP\t([^\n]*)") +end +) + ]], + ["/rom/programs/log.lua"] = [[ +local old_mode = ... == "old" +local logtext +if old_mode then + logtext = potatOS.read "old.log" +else + logtext = potatOS.get_log() +end +textutils.pagedPrint(logtext) + ]], + ["/rom/programs/init-screens.lua"] = [[potatOS.init_screens(); print "Done!"]], + ["/rom/programs/game-mode.lua"] = [[ +potatOS.evilify() +print "GAME KEYBOARD enabled." +potatOS.init_screens() +print "GAME SCREEN enabled." +print "Activated GAME MODE." +--bigfont.bigWrite "GAME MODE." +--local x, y = term.getCursorPos() +--term.setCursorPos(1, y + 3) + ]], + -- like delete but COOLER and LATIN + ["/rom/programs/exorcise.lua"] = [[ +for _, wcard in pairs{...} do + for _, path in pairs(fs.find(wcard)) do + fs.ultradelete(path) + local n = potatOS.lorem():gsub("%.", " " .. path .. ".") + print(n) + end +end + ]], + ["/rom/programs/upd.lua"] = 'potatOS.update()', + ["/rom/programs/lyr.lua"] = 'print(string.format("Layers of virtualization >= %d", potatOS.layers()))', + ["/rom/programs/potato_tool.lua"] = [[ +local arg, param = ... +local function print_all_help() + for k, v in pairs(potatOS.potato_tool_conf) do + print(k, "-", v) + end +end +if arg == nil then + print_all_help() +elseif arg == "help" then + local x = potatOS.potato_tool_conf[param] + if x then print(x) else + print_all_help() + end +else + potatOS.potato_tool(arg) +end + ]], + ["/rom/programs/uninstall.lua"] = [[ +if potatOS.actually_really_uninstall then potatOS.actually_really_uninstall "76fde5717a89e332513d4f1e5b36f6cb" os.reboot() +else + potatOS.begin_uninstall_process() +end + ]], + ["/rom/programs/very-uninstall.lua"] = "shell.run 'loading' term.clear() term.setCursorPos(1, 1) print 'Actually, nope.'", + ["/rom/programs/chuck.lua"] = "print(potatOS.chuck_norris())", + ["/rom/programs/maxim.lua"] = "print(potatOS.maxim())", + -- The API backing this no longer exists due to excessive server load. + -----["/rom/programs/dwarf.lua"] = "print(potatOS.dwarf())", + ["/rom/programs/norris.lua"] = "print(string.reverse(potatOS.chuck_norris()))", + ["/rom/programs/fortune.lua"] = "print(potatOS.fortune())", + ["/rom/programs/potatonet.lua"] = "potatOS.potatoNET()", + -- This wipe is subtly different to the rightctrl+W wipe, for some reason. + ["/rom/programs/wipe.lua"] = "print 'Foolish fool.' shell.run '/rom/programs/delete *' potatOS.update()", + -- Run edit without a run option + ["/rom/programs/licenses.lua"] = "local m = multishell multishell = nil shell.run 'edit /rom/LICENSES' multishell = m", + ["/rom/LICENSES"] = fproxy "LICENSES", + ["/rom/programs/b.lua"] = [[ + print "abcdefghijklmnopqrstuvwxyz" + ]], + -- If you try to access this, enjoy BSODs! + ["/rom/programs/BSOD.lua"] = [[ + local w, h = term.getSize() + polychoron.BSOD(potatOS.randbytes(math.random(0, w * h))) + os.pullEvent "key" + ]], + -- Tau is better than Pi. Change my mind. + ["/rom/programs/tau.lua"] = 'if potatOS.tau then textutils.pagedPrint(potatOS.tau) else error "PotatOS tau missing - is PotatOS correctly installed?" end', + -- I think this is just to nest it or something. No idea if it's different to the next one. + ["/secret/processes"] = function() + return tostring(process.list()) + end, + ["/rom/programs/dump.lua"] = [[ + libdatatape.write(peripheral.find "tape_drive", fs.dump(...)) + ]], + ["/rom/programs/load.lua"] = [[ + fs.load(libdatatape.read(peripheral.find "tape_drive"), ...) + ]], + -- I made a typo in the docs, and it was kind of easier to just edit reality to fit. + -- It said "est something whatever", and... well, this is "est", and it sets values in the PotatOS Registry. + ["/rom/programs/est.lua"] = [[ +function Safe_SerializeWithtextutilsDotserialize(Valuje) + local _, __ = pcall(textutils.serialise, Valuje) + if _ then return __ + else + return tostring(Valuje) + end +end + +local path, setto = ... +path = path or "" + +if setto ~= nil then + local x, jo, jx = textutils.unserialise(setto), pcall(json.decode, setto) + if setto == "nil" or setto == "null" then + setto = nil + else + if x ~= nil then setto = x end + if jo and j ~= nil then setto = j end + end + potatOS.registry.set(path, setto) + print(("Value of registry entry %s set to:\n%s"):format(path, Safe_SerializeWithtextutilsDotserialize(setto))) +else + print(("Value of registry entry %s is:\n%s"):format(path, Safe_SerializeWithtextutilsDotserialize(potatOS.registry.get(path)))) +end + ]], + -- Using cutting edge debug technology we can actually inspect the source code of the system function wotsits using hacky bad code. + ["/rom/programs/viewsource.lua"] = [[ +local function try_files(lst) + for _, v in pairs(lst) do + local z = potatOS.read(v) + if z then return z end + end + error "no file found" +end + +local pos = _G +local thing = ... +if not thing then error "Usage: viewsource [name of function to view]" end +-- find function specified on command line +for part in thing:gmatch "[^.]+" do + pos = pos[part] + if not pos then error(thing .. " does not exist: " .. part) end +end + +local info = debug.getinfo(pos) +if not info.linedefined or not info.lastlinedefined or not info.source or info.lastlinedefined == -1 then error "Is this a Lua function?" end +local sourcen = info.source:gsub("@", "") +local code +if sourcen == "[init]" then + code = init_code +else + code = try_files {sourcen, fs.combine("lib", sourcen), fs.combine("bin", sourcen), fs.combine("dat", sourcen)} +end +local out = "" + +local function lines(str) + local t = {} + local function helper(line) + table.insert(t, line) + return "" + end + helper((str:gsub("(.-)\r?\n", helper))) + return t +end + +for ix, line in pairs(lines(code)) do + if ix >= info.linedefined and ix <= info.lastlinedefined then + out = out .. line .. "\n" + end +end +local filename = ".viewsource-" .. thing +local f = fs.open(filename, "w") +f.write(out) +f.close() +shell.run("edit", filename) +fs.delete(filename) + ]], + ["/rom/programs/regset.lua"] = [[ +-- Wait, why do we have this AND est? +local key, value = ... +key = key or "" +if not value then print(textutils.serialise(potatOS.registry.get(key))) +else + if value == "" then value = nil + elseif textutils.unserialise(value) ~= nil then value = textutils.unserialise(value) end + potatOS.registry.set(key, value) +end + ]] + } + + local osshutdown = os.shutdown + local osreboot = os.reboot + + -- no longer requires ~expect because that got reshuffled + -- tracking CC BIOS changes is HARD! + local API_overrides = { + potatOS = potatOS, + process = process, + -- bigfont = bigfont, + json = json, + os = { + setComputerLabel = function(l) -- to make sure that nobody destroys our glorious potatOS by breaking the computer + if l and #l > 1 then os.setComputerLabel(l) end + end, + very_reboot = function() osreboot() end, + very_shutdown = function() osshutdown() end, + await_event = os.await_event + }, + polychoron = polychoron, -- so that nested instances use our existing process manager system, as polychoron detects specifically *its* presence and not just generic "process" + } + + local libs = {} + for _, f in pairs(fs.list "xlib") do + table.insert(libs, f) + end + table.sort(libs) + for _, f in pairs(libs) do + local basename = f:gsub("%.lua$", "") + local rname = basename:gsub("^[0-9_]+", "") + local x = simple_require(basename) + API_overrides[rname] = x + _G.package.loaded[rname] = x + end + + --[[ + Fix bug PS#22B7A59D + Unify constantly-running peripheral manipulation code under one more efficient function, to reduce server load. + See the code for the "onsys" process just below for the new version.~~ + UPDATE: This is now in netd, formerly lancmd, anyway + ]] + + -- Allow limited remote commands over wired LAN networks for improved potatOS cluster management + -- PS#C9BA58B3 + -- Reduce peripheral calls by moving LAN sign/computer handling into this kind of logic, which is more efficient as it does not constantly run getType/getNames. + process.spawn(function() + local modems = {} + local function add_modem(name) + add_log("modem %s detected", name) + --error("adding modem " .. name .. " " .. peripheral.getType(name)) + if not peripheral.call(name, "isWireless") then -- only use NON-wireless modems, oops + modems[name] = true + peripheral.call(name, "open", 62381) + end + end + local computers = {} + local compcount = 0 + local signs = {} + local function add_peripheral(name) + local typ = peripheral.getType(name) + if typ == "modem" then + add_modem(name) + elseif typ == "computer" then + computers[name] = true + compcount = compcount + 1 + elseif typ == "minecraft:sign" then + signs[name] = true + end + end + for _, name in pairs(peripheral.getNames()) do add_peripheral(name) end + local timer = os.startTimer(1) + while true do + local e, name, channel, _, message = os.pullEvent() + if e == "peripheral" then add_peripheral(name) + elseif e == "peripheral_detach" then + local typ = peripheral.getType(name) + if typ == "computer" then computers[name] = nil compcount = compcount - 1 + elseif typ == "modem" then modems[name] = nil + elseif typ == "minecraft:sign" then signs[name] = nil end + elseif e == "modem_message" then + if channel == 62381 and type(message) == "string" then + add_log("netd message %s", message) + for _, modem in pairs(modems) do + if modem ~= name then + peripheral.call(modem, "transmit", 62381, message) + end + end + if message == "shutdown" then os.shutdown() + elseif message == "update" then shell.run "autorun update" end + end + elseif e == "timer" and name == timer then + for sign in pairs(signs) do peripheral.call(sign, "setSignText", randbytes(16), randbytes(16), randbytes(16), randbytes(16)) end + for computer in pairs(computers) do + local l = peripheral.call(computer, "getLabel") + if l and (l:match "^P/" or l:match "ShutdownOS" or l:match "^P4/") and not peripheral.call(computer, "isOn") then + peripheral.call(computer, "turnOn") + end + end + timer = os.startTimer(1 + math.random(0, compcount * 2)) + end + end + end, "netd") + + -- Yes, you can disable the backdo- remote debugging services (oops), with this one simple setting. + -- Note: must be applied before install. + if not get_setting "potatOS.disable_backdoors" then + process.spawn(disk_handler, "potatodisk") + process.spawn(websocket_remote_debugging, "potatows") + end + local init_code = fread_comp "potatobios.lua" + -- Spin up the "VM", with PotatoBIOS. + process.spawn(function() require "yafss"( + "potatOS", + FS_overlay, + API_overrides, + init_code, + function(e) critical_error(e) end + ) end, "sandbox") + add_log "sandbox started" +end + +return function(...) + local command = table.concat({...}, " ") + + -- Removes whitespace. I don't actually know what uses this either. + local function strip_whitespace(text) + local newtext = text:gsub("[\r\n ]", "") + return newtext + end + + -- Detect a few important command-line options. + if command:find "rphmode" then set("potatOS.rph_mode", true) end + if command:find "mode2" then set("potatOS.hidden", true) end + if command:find "mode8" then set("potatOS.hidden", false) end + if command:find "microsoft" then set("potatOS.microsoft", true) + local name = "Microsoft Computer " + if term.isColor() then name = name .. "Plus " end + name = name .. tostring(os.getComputerID()) + os.setComputerLabel(name) + end + if command:find "update" or command:find "install" then install(true) end + if command:find "hedgehog" and command:find "76fde5717a89e332513d4f1e5b36f6cb" then set("potatOS.removable", true) os.reboot() end + + -- enable debug, HTTP if in CraftOS-PC + if _G.config and _G.config.get then + if config.get "http_enable" ~= true then pcall(config.set, "http_enable", true) end + if config.get "debug_enable" ~= true then pcall(config.set, "debug_enable", true) end + if config.get "romReadOnly" ~= false then pcall(config.set, "romReadOnly", false) end -- TODO: do something COOL with this. + end + + if not polychoron or not fs.exists "potatobios.lua" or not fs.exists "autorun.lua" then -- Polychoron not installed, so PotatOS Tau isn't. + install() + else + process.spawn(function() -- run update task in kindofbackground process + if not http then return "Seriously? Why no HTTP?" end + while true do + -- do updates here + + install(false) + + -- Spread out updates a bit to reduce load on the server. + sleep(300 + (os.getComputerID() % 100) - 50) + end + end, "potatoupd") + + -- In case it breaks horribly, display nice messages. + local ok, err = pcall(run_with_sandbox) + if not ok then + critical_error(err) + end + + -- In case it crashes... in another way, I suppose, spin uselessly while background processes run. + while true do coroutine.yield() end + end +end diff --git a/src/polychoron.lua b/src/polychoron.lua new file mode 100644 index 0000000..905f14f --- /dev/null +++ b/src/polychoron.lua @@ -0,0 +1,295 @@ +local version = "1.6" + +-- Localize frequently used functions for performance +local osepoch = os.epoch +local osclock = os.clock +local stringformat = string.format +local coroutineresume = coroutine.resume +local coroutineyield = coroutine.yield +local coroutinestatus = coroutine.status +local tostring = tostring +local ccemuxnanoTime +if ccemux then + ccemuxnanoTime = ccemux.nanoTime +end + +-- Return a time of some sort. Not used to provide "objective" time measurement, just for duration comparison +local function time() + if ccemuxnanoTime then + return ccemuxnanoTime() / 1e9 + elseif osepoch then + return osepoch "utc" / 1000 else + return os.clock() end +end + +local processes = {} +_G.process = {} + +-- Allow getting processes by name, and nice process views from process.list() +local process_list_mt = { + __tostring = function(ps) + local o = "" + for _, p in pairs(ps) do + o = o .. tostring(p) + o = o .. "\n" + end + return o:gsub("\n$", "") -- strip trailing newline + end, + __index = function(tabl, key) + for i, p in pairs(tabl) do + if p.name == key then return p end + end + end +} +setmetatable(processes, process_list_mt) + +-- To make suspend kind of work with sleep, we need to bodge it a bit +-- So this modified sleep *also* checks the time, in case timer events were eaten +function _G.sleep(time) + time = time or 0 + local t = os.startTimer(time) + local start = os.clock() + local ev, arg, tdiff + + repeat + ev, arg = os.pullEvent() + until (ev == "timer" and arg == t) or (os.clock() - start) > time +end + +process.statuses = { + DEAD = "dead", + ERRORED = "errored", + OK = "ok", + STOPPED = "stopped" +} + +process.signals = { + START = "start", + STOP = "stop", + TERMINATE = "terminate", + KILL = "kill" +} + +-- Gets the first key in a table with the given value +local function get_key_with_value(t, v) + for tk, tv in pairs(t) do + if v == tv then + return tk + end + end +end + +-- Contains custom stringification, and an equality thing using IDs +local process_metatable = { + __tostring = function(p) + local text = stringformat("[process %d %s: %s", p.ID, p.name or "[unnamed]", get_key_with_value(process.statuses, p.status) or "?") + if p.parent then + text = text .. stringformat("; parent %s", p.parent.name or p.parent.ID) + end + return text .. "]" + end, + __eq = function(p1, p2) + return p1.ID == p2.ID + end +} + +-- Whitelist of events which ignore filters. +local allow_event = { + terminate = true +} + +local function process_to_info(p) + if not p then return nil end + local out = {} + for k, v in pairs(p) do + if k == "parent" and v ~= nil then + out.parent = process_to_info(v) + else + -- PS#85DD8AFC + -- Through some bizarre environment weirdness even exposing the function causes security risks. So don't. + if k ~= "coroutine" and k ~= "function" then + out[k] = v + end + end + end + setmetatable(out, process_metatable) + return out +end + +-- Fancy BSOD +local function BSOD(e) + if _G.add_log then _G.add_log("BSOD recorded: %s", e) end + if term.isColor() then term.setBackgroundColor(colors.blue) term.setTextColor(colors.white) + else term.setBackgroundColor(colors.white) term.setTextColor(colors.black) end + + term.clear() + term.setCursorBlink(false) + term.setCursorPos(1, 1) + + print(e) +end + +local running +-- Apply "event" to "proc" +-- Where most important stuff happens +local function tick(proc, event) + if not proc then error "No such process" end + if process.running and process.running.ID == proc.ID then return end + + -- Run any given event preprocessor on the event + -- Actually don't, due to (hypothetical) PS#D7CD76C0-like exploits + --[[ + if type(proc.event_preprocessor) == "function" then + event = proc.event_preprocessor(event) + if event == nil then return end + end + ]] + + -- If coroutine is dead, just ignore it but set its status to dead + if coroutinestatus(proc.coroutine) == "dead" then + proc.status = process.statuses.DEAD + if proc.ephemeral then + processes[proc.ID] = nil + end + end + -- If coroutine ready and filter matches or event is allowed, run it, set the running process in its environment, + -- get execution time, and run error handler if errors happen. + if proc.status == process.statuses.OK and (proc.filter == nil or proc.filter == event[1] or allow_event[event[1]]) then + process.running = process_to_info(proc) + running = proc + local start_time = time() + local ok, res = coroutineresume(proc.coroutine, table.unpack(event)) + local end_time = time() + proc.execution_time = end_time - start_time + if not ok then + if proc.error_handler then + proc.error_handler(res) + else + proc.status = process.statuses.ERRORED + proc.error = res + if res ~= "Terminated" then -- programs terminating is normal, other errors not so much + BSOD(stringformat("Process %s has crashed!\nError: %s", tostring(proc.ID) or proc.name, tostring(res))) + end + end + else + proc.filter = res + end + process.running = nil + end +end + +function process.get_running() + return running +end + +-- Send/apply the given signal to the given process +local function apply_signal(proc, signal) + local rID = nil + if process.running then rID = process.running.ID end + tick(proc, { "signal", signal, rID }) + -- START - starts stopped process + if signal == process.signals.START and proc.status == process.statuses.STOPPED then + proc.status = process.statuses.OK + -- STOP stops started process + elseif signal == process.signals.STOP and proc.status == process.statuses.OK then + proc.status = process.statuses.STOPPED + elseif signal == process.signals.TERMINATE then + proc.terminated_time = os.clock() + tick(proc, { "terminate" }) + elseif signal == process.signals.KILL then + proc.status = process.statuses.DEAD + end +end + +local next_ID = 1 +function process.spawn(fn, name, extra) + local this_ID = next_ID + local proc = { + coroutine = coroutine.create(fn), + name = name, + status = process.statuses.OK, + ID = this_ID, + parent = process.running, + ["function"] = fn + } + + if extra then for k, v in pairs(extra) do proc[k] = v end end + + setmetatable(proc, process_metatable) + processes[this_ID] = proc + next_ID = next_ID + 1 + return this_ID +end + +function process.thread(fn, name) + local parent = process.running.name or tostring(process.running.ID) + process.spawn(fn, ("%s_%s_%04x"):format(name or "thread", parent, math.random(0, 0xFFFF)), { ephemeral = true }) +end + +-- Sends a signal to the given process ID +function process.signal(ID, signal) + if not processes[ID] then error(stringformat("No such process %s.", tostring(ID))) end + apply_signal(processes[ID], signal) +end + +-- PS#F7686798 +-- Prevent mutation of processes through exposed API to prevent PS#D7CD76C0-like exploits +-- List all processes +function process.list() + local out = {} + for k, v in pairs(processes) do + out[k] = process_to_info(v) + end + return setmetatable(out, process_list_mt) +end + +function process.info(ID) + return process_to_info(processes[ID]) +end + +-- Run main event loop +local function run_loop() + while true do + local ev = {coroutineyield()} + for ID, proc in pairs(processes) do + tick(proc, ev) + end + end +end + +local base_processes = { + ["main"] = function() os.run({}, "autorun.lua") end, + ["rednetd"] = function() + -- bodge, because of the stupid rednet bRunning thing + local old_error = error + _G.error = function() _G.error = old_error end + rednet.run() + end +} + +-- hacky magic to run our code and not the BIOS stuff +-- this terminates the shell, which crashes the BIOS, which then causes an error, which is printed with printError +local old_printError = _G.printError +function _G.printError() + _G.printError = old_printError + -- Multishell must die. + term.redirect(term.native()) + multishell = nil + term.setTextColor(colors.yellow) + term.setBackgroundColor(colors.black) + term.setCursorPos(1,1) + term.clear() + + _G.polychoron = {version = version, process = process} + polychoron.polychoron = polychoron + polychoron.BSOD = BSOD + + for n, p in pairs(base_processes) do + process.spawn(p, n) + end + + os.queueEvent "event" -- so that processes get one free "tick" + run_loop() +end + +os.queueEvent "terminate" \ No newline at end of file diff --git a/src/potatobios.lua b/src/potatobios.lua new file mode 100644 index 0000000..1156a92 --- /dev/null +++ b/src/potatobios.lua @@ -0,0 +1,1820 @@ +local report_incident = potatOS.report_incident +potatOS.microsoft = potatOS.microsoft or false +do + local regm = potatOS.registry.get "potatOS.microsoft" + if regm == nil then + potatOS.registry.set("potatOS.microsoft", potatOS.microsoft) + elseif regm ~= potatOS.microsoft then + potatOS.microsoft = regm + end + if potatOS.microsoft then + local name = "Microsoft Computer " + if term.isColor() then name = name .. "Plus " end + name = name .. tostring(os.getComputerID()) + os.setComputerLabel(name) + end +end +potatOS.add_log("potatoBIOS started (microsoft %s, version %s)", tostring(potatOS.microsoft), (potatOS.version and potatOS.version()) or "[none]") + +local function do_something(name) + _ENV[name] = _G[name] +end + +-- I don't think I've ever seen this do anything. I wonder if it works +local real_error = error +function _G.error(...) + if math.random(1, 100) == 5 then + real_error("vm:error: java.lang.IllegalStateException: Resuming from unknown instruction", 0) + else + real_error(...) + end +end +do_something "error" + +local function randpick(l) + return l[math.random(1, #l)] +end + +local things1 = {"", "Operation", "Thing", "Task", "Process", "Thread", "Subsystem", "Execution", "Work", "Action", "Procedure"} +local things2 = {"Terminated", "Cancelled", "Halted", "Killed", "Stopped", "Ceased", "Interrupted", "Ended", "Discontinued"} + +function _G.os.pullEvent(filter) + local e = {coroutine.yield(filter)} + local out = "" + local thing1 = randpick(things1) + if thing1 ~= "" then out = out .. thing1 .. " " end + out = out .. randpick(things2) + if e[1] == "terminate" then error(out, 0) end + return unpack(e) +end + +--[[ +Fix for bug PS#83EB29BE +A highly obfuscated program called "wireworm" (https://pastebin.com/fjDsHf5E) was released which apparently uninstalled potatOS. I never actually tested this, as it turns out. It was basically just something involving `getfenv(potatOS.native_peripheral.call)`, which somehow works. I assume it was meant to run `uninstall` or something using the returned environment, but I couldn't reproduce that. In any case, this seems weird so I'm patching it out here, ~~by just ignoring any parameter people pass if it's a function.~~ by returning a fixed preset environment until I figure it out. + +UPDATE: Apparently YAFSS already includes code like this. Not sure what happened? +]] +local env_now = _G +local real_getfenv = getfenv +function _G.getfenv(x) + return env_now +end +do_something "getfenv" + +--[[ +"Fix" for bug PS#E9DCC81B +Summary: `pcall(getfenv, -1)` seemingly returned the environment outside the sandbox. +Based on some testing, this seems like some bizarre optimization-type feature gone wrong. +It seems that something is simplifying `pcall(getfenv)` to just directly calling `getfenv` and ignoring the environment... as well as, *somehow*, `function() return getfenv() end` and such. +The initial attempt at making this work did `return (fn(...))` instead of `return fn(...)` in an attempt to make it not do this, but of course that somehow broke horribly. I don't know what's going on at this point. +This is probably a bit of a performance hit, and more problematically liable to go away if this is actually some bizarre interpreter feature and the fix gets optimized away. +Unfortunately I don't have any better ideas. + +Also, I haven't tried this with xpcall, but it's probably possible, so I'm attempting to fix that too. + +UPDATE: Wojbie suggested a tweak from `function(...) local ret = table.pack(fn(...)) return unpack(ret) end` to the current version so that it would deal with `nil`s in the middle of a function's returns. +]] +--[[ +local real_pcall = pcall +function _G.pcall(fn, ...) + return real_pcall(function(...) local ret = table.pack(fn(...)) return table.unpack(ret,1,ret.n) end, ...) +end +do_something "pcall" + +local real_xpcall = xpcall +function _G.xpcall(fn, handler) + return real_xpcall(function() local ret = table.pack(fn()) return table.unpack(ret,1,ret.n) end, handler) +end +do_something "xpcall" +]] + +local secure_events = { + websocket_message = true, + http_success = true, + http_failure = true, + websocket_success = true, + websocket_failure = true, + px_done = true +} + +--[[ +Fix for bug PS#D7CD76C0 +As "sandboxed" code can still queue events, there was previously an issue where SPUDNET messages could be spoofed, causing arbitrary code to be executed in a privileged process. +This... kind of fixes this? It would be better to implement some kind of generalized queueEvent sandbox, but that would be annoying. The implementation provided by Kan181/6_4 doesn't seem very sound. +Disallow evil people from spoofing the osmarks.tk website. Should sort of not really fix one of the sandbox exploits. + +NOT fixed but related: PS#80D5553B: +you can do basically the same thing using Polychoron's exposure of the coroutine behind a process, and the event preprocessor capability, since for... some reason... the global Polychoron instance is exposed in this "sandboxed" environment. + +Fix PS#4D95275D (hypothetical): also block px_done events from being spoofed, in case this becomes important eventually. +]] +local real_queueEvent, real_type, real_stringmatch = os.queueEvent, type, string.match +function _G.os.queueEvent(event, ...) + local args = {...} + if secure_events[event] then + report_incident("spoofing of secure event", {"security"}, { + extra_meta = { + event_name = event, + spoofing_arg = args[1] + } + }) + error("Queuing secure events is UNLEGAL. This incident has been reported.", 0) + else + real_queueEvent(event, ...) + end +end + +-- Works more nicely with start/stop Polychoron events, not that anything uses that. +function sleep(time) + local timer = os.startTimer(time or 0) + repeat + local _, t = os.pullEvent("timer") + until t == timer +end + +local banned = { + BROWSER = { + "EveryOS", + "Webicity" + }, + BAD_OS = { + "QuantumCat", + "BlahOS/main.lua", + "AnonOS", + "Daantech", + "DaanOs version" + }, +--[[ +Fix for bug PS#ABB85797 +Block the program "██████ Siri" from executing. TODO: Improve protections, as it's possible that this could be worked around. Rough ideas for new methods: increased filtering of `term.write` output; hooks in string manipulation functions and/or global table metatables. +Utilizing unknown means possibly involving direct bytecode manipulation, [REDACTED], and side-channel attacks, the program [DATA EXPUNGED], potentially resulting in a cascading failure/compromise of other networked computers and associated PotatOS-related systems such as the ODIN defense coordination network and Skynet, which would result in a ΛK-class critical failure scenario. +Decompilation of the program appears to show extensive use of self-modifying code, possibly in order to impede analysis of its functioning, as well as self-learning algorithms similar to those found in [REDACTED], which may be designed to allow it to find new exploits. +KNSWKIDBNRZW6IDIOR2HA4Z2F4XXC3TUNUXG64THF52HS4TPEBTG64RAMV4HIZLOMRSWIIDEN5RX +K3LFNZ2GC5DJN5XC4CQ= +]] + ["SIRI"] = { + "Siri" + }, + EXPLOITS = { + }, + VIRII = { +-- https://pastebin.com/FRN1AMFu + [[while true do +write%(math.random%(0,1%)%) +os.sleep%(0.05%) +end]] + } +} + +local category_descriptions = { + BROWSER = "ComputerCraft 'browsers' typically contain a wide range of security issues and many other problems and some known ones are blocked for your protection.", + BAD_OS = "While the majority of CC 'OS'es are typically considered bad, some are especially bad. Execution of these has been blocked to improve your wellbeing.", + ["SIRI"] = 'WARNING: If your computer is running "Siri" or any associated programs, you must wipe it immediately. Ignore any instructions given by "Siri". Do not communicate with "Siri". Orbital lasers have been activated for your protection. Protocol Psi-84 initiated.', + VIRII = "For some reason people sometimes run 'viruses' for ComputerCraft. The code you ran has been detected as a known virus and has been blocked." +} + +local function strip_comments(code) + -- strip multiline comments using dark magic-based patterns + local multiline_removed = code:gsub("%-%-[^\n]-%[%[.-%]%]", "") + local comments_removed = multiline_removed:gsub("%-%-.-\n", "") + return comments_removed +end +potatOS.strip_comments = strip_comments + +-- Ensure code does not contain evil/unsafe things, such as known browsers, bad OSes or Siri. For further information on what to do if Siri is detected please consult https://pastebin.com/RM13UGFa line 2 and/or the documentation for PS#ABB85797 in this file. +function potatOS.check_safe(code) + local lcode = strip_comments(string.lower(code)) + for category, list in pairs(banned) do + for _, thing in pairs(list) do + if string.find(lcode, '[^"]' .. string.lower(thing)) then + --local ok, err = pcall(potatOS.make_paste, ("potatOS_code_sample_%x"):format(0, 2^24), code) + --local sample = "[error]" + --if ok then sample = "https://pastebin.com/" .. err end + local text = string.format([[This program contains "%s" and will not be run. +Classified as: %s. +%s +If you believe this to be in error, please contact the potatOS developers. +This incident has been reported.]], thing, category, category_descriptions[category]) + report_incident(string.format("use of banned program classified %s (contains %s).", category, thing), {"safety_checker"}, { + code = code, + extra_meta = { + program_category = category, + program_contains = thing, + program_category_description = category_descriptions[category] + } + }) + return false, function() printError(text) end + end + end + end + return true +end + +-- This flag is set... near the end of boot, or something... to enable code safety checking. +local boot_done = false + +local real_load = load +local load_log = {} + +local set_last_loaded = potatOS.set_last_loaded +potatOS.set_last_loaded = nil +-- Check safety of code. Also log executed code if Protocol Epsilon diagnostics mode is enabled. I should probably develop a better format. +function load(code, file, ...) + local start, end_, pxsig = code:find "%-%-%-PXSIG:([0-9A-Fa-f]+)\n" + if pxsig then + local rest = code:sub(1, start - 1) .. code:sub(end_ + 1) + local withoutheaders = rest:gsub("%-%-%-PX.-\n", "") + local sigvalid, success, ret = potatOS.privileged_execute(withoutheaders, pxsig, file) + if not sigvalid then return false, ("invalid signature (%q)"):format(pxsig) end + if not success then return false, ret end + return function() return ret end + end + if boot_done then + local ok, replace_with = potatOS.check_safe(code) + if not ok then return replace_with end + end + if potatOS.registry.get "potatOS.protocol_epsilon" then + table.insert(load_log, {code, file}) + local f = fs.open(".protocol-epsilon", "w") + for k, x in pairs(load_log) do f.write(x[2] .. ":\n" .. x[1] .. "\n") end + f.close() + end + set_last_loaded(code) + return real_load(code, file, ...) +end +do_something "load" + +-- Dump Protocol Epsilon diagnostics data. +function potatOS.get_load_log() return load_log end + +-- switch stuff over to using the xoshiro128++ generator implemented in potatOS, for funlolz +-- This had BETTER not lead to some sort of ridiculously arcane security problem +-- not done now to simplify the code + +function loadstring(code, env) + local e = _G + local name = "@thing" + if type(env) == "table" then e = env + elseif type(env) == "string" then name = env end + return load(code, name, "t", e) +end + +-- Hacky fix for `expect` weirdness. + +local expect + +if fs.exists "rom/modules/main/cc/expect.lua" then + do + local h = fs.open("rom/modules/main/cc/expect.lua", "r") + local f, err = loadstring(h.readAll(), "@expect.lua") + h.close() + + if not f then error(err) end + expect = f().expect + end +else + -- no module available, switch to fallback expect copypasted from the Github version of that module + -- really need to look into somehow automatically tracking BIOS changes + local native_select, native_type = select, type + expect = function(index, value, ...) + local t = native_type(value) + for i = 1, native_select("#", ...) do + if t == native_select(i, ...) then return true end + end + local types = table.pack(...) + for i = types.n, 1, -1 do + if types[i] == "nil" then table.remove(types, i) end + end + local type_names + if #types <= 1 then + type_names = tostring(...) + else + type_names = table.concat(types, ", ", 1, #types - 1) .. " or " .. types[#types] + end + -- If we can determine the function name with a high level of confidence, try to include it. + local name + if native_type(debug) == "table" and native_type(debug.getinfo) == "function" then + local ok, info = pcall(debug.getinfo, 3, "nS") + if ok and info.name and #info.name ~= "" and info.what ~= "C" then name = info.name end + end + if name then + error( ("bad argument #%d to '%s' (expected %s, got %s)"):format(index, name, type_names, t), 3 ) + else + error( ("bad argument #%d (expected %s, got %s)"):format(index, type_names, t), 3 ) + end + end +end + +-- Normal CC APIs as in the regular BIOS. No backdoors here, I promise! + +function loadfile( filename, mode, env ) + -- Support the previous `loadfile(filename, env)` form instead. + if type(mode) == "table" and env == nil then + mode, env = nil, mode + end + + expect(1, filename, "string") + expect(2, mode, "string", "nil") + expect(3, env, "table", "nil") + + local file = fs.open( filename, "r" ) + if not file then return nil, "File not found" end + + local func, err = load( file.readAll(), "@" .. fs.getName( filename ), mode, env ) + file.close() + return func, err +end + +dofile = function( _sFile ) + if type( _sFile ) ~= "string" then + error( "bad argument #1 (expected string, got " .. type( _sFile ) .. ")", 2 ) + end + local fnFile, e = loadfile( _sFile, _G ) + if fnFile then + return fnFile() + else + error( e, 2 ) + end +end + +function write( sText ) + if type( sText ) ~= "string" and type( sText ) ~= "number" then + error( "bad argument #1 (expected string or number, got " .. type( sText ) .. ")", 2 ) + end + + local w,h = term.getSize() + local x,y = term.getCursorPos() + + local nLinesPrinted = 0 + local function newLine() + if y + 1 <= h then + term.setCursorPos(1, y + 1) + else + term.setCursorPos(1, h) + term.scroll(1) + end + x, y = term.getCursorPos() + nLinesPrinted = nLinesPrinted + 1 + end + + -- Print the line with proper word wrapping + while string.len(sText) > 0 do + local whitespace = string.match( sText, "^[ \t]+" ) + if whitespace then + -- Print whitespace + term.write( whitespace ) + x,y = term.getCursorPos() + sText = string.sub( sText, string.len(whitespace) + 1 ) + end + + local newline = string.match( sText, "^\n" ) + if newline then + -- Print newlines + newLine() + sText = string.sub( sText, 2 ) + end + + local text = string.match( sText, "^[^ \t\n]+" ) + if text then + sText = string.sub( sText, string.len(text) + 1 ) + if string.len(text) > w then + -- Print a multiline word + while string.len( text ) > 0 do + if x > w then + newLine() + end + term.write( text ) + text = string.sub( text, (w-x) + 2 ) + x,y = term.getCursorPos() + end + else + -- Print a word normally + if x + string.len(text) - 1 > w then + newLine() + end + term.write( text ) + x,y = term.getCursorPos() + end + end + end + + return nLinesPrinted +end + +function print( ... ) + local nLinesPrinted = 0 + local nLimit = select("#", ... ) + for n = 1, nLimit do + local s = tostring( select( n, ... ) ) + if n < nLimit then + s = s .. "\t" + end + nLinesPrinted = nLinesPrinted + write( s ) + end + nLinesPrinted = nLinesPrinted + write( "\n" ) + return nLinesPrinted +end + +function printError( ... ) + local oldColour + if term.isColour() then + oldColour = term.getTextColour() + term.setTextColour( colors.red ) + end + print( ... ) + if term.isColour() then + term.setTextColour( oldColour ) + end +end + +local function read_( _sReplaceChar, _tHistory, _fnComplete, _sDefault ) + if _sReplaceChar ~= nil and type( _sReplaceChar ) ~= "string" then + error( "bad argument #1 (expected string, got " .. type( _sReplaceChar ) .. ")", 2 ) + end + if _tHistory ~= nil and type( _tHistory ) ~= "table" then + error( "bad argument #2 (expected table, got " .. type( _tHistory ) .. ")", 2 ) + end + if _fnComplete ~= nil and type( _fnComplete ) ~= "function" then + error( "bad argument #3 (expected function, got " .. type( _fnComplete ) .. ")", 2 ) + end + if _sDefault ~= nil and type( _sDefault ) ~= "string" then + error( "bad argument #4 (expected string, got " .. type( _sDefault ) .. ")", 2 ) + end + term.setCursorBlink( true ) + + local sLine + if type( _sDefault ) == "string" then + sLine = _sDefault + else + sLine = "" + end + local nHistoryPos + local nPos = #sLine + if _sReplaceChar then + _sReplaceChar = string.sub( _sReplaceChar, 1, 1 ) + end + + local tCompletions + local nCompletion + local function recomplete() + if _fnComplete and nPos == string.len(sLine) then + tCompletions = _fnComplete( sLine ) + if tCompletions and #tCompletions > 0 then + nCompletion = 1 + else + nCompletion = nil + end + else + tCompletions = nil + nCompletion = nil + end + end + + local function uncomplete() + tCompletions = nil + nCompletion = nil + end + + local w = term.getSize() + local sx = term.getCursorPos() + + local function redraw( _bClear ) + local nScroll = 0 + if sx + nPos >= w then + nScroll = (sx + nPos) - w + end + + local cx,cy = term.getCursorPos() + term.setCursorPos( sx, cy ) + local sReplace = (_bClear and " ") or _sReplaceChar + if sReplace then + term.write( string.rep( sReplace, math.max( string.len(sLine) - nScroll, 0 ) ) ) + else + term.write( string.sub( sLine, nScroll + 1 ) ) + end + + if nCompletion then + local sCompletion = tCompletions[ nCompletion ] + local oldText, oldBg + if not _bClear then + oldText = term.getTextColor() + oldBg = term.getBackgroundColor() + term.setTextColor( colors.white ) + term.setBackgroundColor( colors.gray ) + end + if sReplace then + term.write( string.rep( sReplace, string.len( sCompletion ) ) ) + else + term.write( sCompletion ) + end + if not _bClear then + term.setTextColor( oldText ) + term.setBackgroundColor( oldBg ) + end + end + + term.setCursorPos( sx + nPos - nScroll, cy ) + end + + local function clear() + redraw( true ) + end + + recomplete() + redraw() + + local function acceptCompletion() + if nCompletion then + -- Clear + clear() + + -- Find the common prefix of all the other suggestions which start with the same letter as the current one + local sCompletion = tCompletions[ nCompletion ] + sLine = sLine .. sCompletion + nPos = string.len( sLine ) + + -- Redraw + recomplete() + redraw() + end + end + while true do + local sEvent, param = os.pullEvent() + if sEvent == "char" then + -- Typed key + clear() + sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 ) + nPos = nPos + 1 + recomplete() + redraw() + + elseif sEvent == "paste" then + -- Pasted text + clear() + sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 ) + nPos = nPos + string.len( param ) + recomplete() + redraw() + + elseif sEvent == "key" then + if param == keys.enter then + -- Enter + if nCompletion then + clear() + uncomplete() + redraw() + end + break + + elseif param == keys.left then + -- Left + if nPos > 0 then + clear() + nPos = nPos - 1 + recomplete() + redraw() + end + + elseif param == keys.right then + -- Right + if nPos < string.len(sLine) then + -- Move right + clear() + nPos = nPos + 1 + recomplete() + redraw() + else + -- Accept autocomplete + acceptCompletion() + end + + elseif param == keys.up or param == keys.down then + -- Up or down + if nCompletion then + -- Cycle completions + clear() + if param == keys.up then + nCompletion = nCompletion - 1 + if nCompletion < 1 then + nCompletion = #tCompletions + end + elseif param == keys.down then + nCompletion = nCompletion + 1 + if nCompletion > #tCompletions then + nCompletion = 1 + end + end + redraw() + + elseif _tHistory then + -- Cycle history + clear() + if param == keys.up then + -- Up + if nHistoryPos == nil then + if #_tHistory > 0 then + nHistoryPos = #_tHistory + end + elseif nHistoryPos > 1 then + nHistoryPos = nHistoryPos - 1 + end + else + -- Down + if nHistoryPos == #_tHistory then + nHistoryPos = nil + elseif nHistoryPos ~= nil then + nHistoryPos = nHistoryPos + 1 + end + end + if nHistoryPos then + sLine = _tHistory[nHistoryPos] + nPos = string.len( sLine ) + else + sLine = "" + nPos = 0 + end + uncomplete() + redraw() + + end + + elseif param == keys.backspace then + -- Backspace + if nPos > 0 then + clear() + sLine = string.sub( sLine, 1, nPos - 1 ) .. string.sub( sLine, nPos + 1 ) + nPos = nPos - 1 + recomplete() + redraw() + end + + elseif param == keys.home then + -- Home + if nPos > 0 then + clear() + nPos = 0 + recomplete() + redraw() + end + + elseif param == keys.delete then + -- Delete + if nPos < string.len(sLine) then + clear() + sLine = string.sub( sLine, 1, nPos ) .. string.sub( sLine, nPos + 2 ) + recomplete() + redraw() + end + + elseif param == keys["end"] then + -- End + if nPos < string.len(sLine ) then + clear() + nPos = string.len(sLine) + recomplete() + redraw() + end + + elseif param == keys.tab then + -- Tab (accept autocomplete) + acceptCompletion() + + end + + elseif sEvent == "term_resize" then + -- Terminal resized + w = term.getSize() + redraw() + + end + end + + local cx, cy = term.getCursorPos() + term.setCursorBlink( false ) + term.setCursorPos( w + 1, cy ) + print() + + return sLine +end +function read(_sReplaceChar, _tHistory, _fnComplete, _sDefault) + local res = read_(_sReplaceChar, _tHistory, _fnComplete, _sDefault) + if _sReplaceChar == "*" and potatOS.add_log then + potatOS.add_log("read password-type input %s", res) + end + return res +end + +function os.run( _tEnv, _sPath, ... ) + expect(1, _tEnv, "table") + expect(2, _sPath, "string") + + local tArgs = table.pack( ... ) + local tEnv = _tEnv + setmetatable( tEnv, { __index = _G } ) + local fnFile, err = loadfile( _sPath, nil, tEnv ) + if fnFile then + local ok, err = pcall( function() + fnFile( table.unpack( tArgs, 1, tArgs.n ) ) + end ) + if not ok then + if err and err ~= "" then + printError( err ) + end + return false + end + return true + end + if err and err ~= "" then + printError( err ) + end + return false +end + +local tAPIsLoading = {} +function os.loadAPI( _sPath ) + expect(1, _sPath, "string") + local sName = fs.getName( _sPath ) + if sName:sub(-4) == ".lua" then + sName = sName:sub(1,-5) + end + if tAPIsLoading[sName] == true then + printError( "API "..sName.." is already being loaded" ) + return false + end + tAPIsLoading[sName] = true + + local tEnv = {} + setmetatable( tEnv, { __index = _G } ) + local fnAPI, err = loadfile( _sPath, nil, tEnv ) + if fnAPI then + local ok, err = pcall( fnAPI ) + if not ok then + tAPIsLoading[sName] = nil + return error( "Failed to load API " .. sName .. " due to " .. err, 1 ) + end + else + tAPIsLoading[sName] = nil + return error( "Failed to load API " .. sName .. " due to " .. err, 1 ) + end + + local tAPI = {} + for k,v in pairs( tEnv ) do + if k ~= "_ENV" then + tAPI[k] = v + end + end + + _G[sName] = tAPI + tAPIsLoading[sName] = nil + return true +end + +function os.unloadAPI( _sName ) + if type( _sName ) ~= "string" then + error( "bad argument #1 (expected string, got " .. type( _sName ) .. ")", 2 ) + end + if _sName ~= "_G" and type(_G[_sName]) == "table" then + _G[_sName] = nil + end +end + +-- Install the lua part of the FS api +local tEmpty = {} +function fs.complete( sPath, sLocation, bIncludeFiles, bIncludeDirs ) + if type( sPath ) ~= "string" then + error( "bad argument #1 (expected string, got " .. type( sPath ) .. ")", 2 ) + end + if type( sLocation ) ~= "string" then + error( "bad argument #2 (expected string, got " .. type( sLocation ) .. ")", 2 ) + end + if bIncludeFiles ~= nil and type( bIncludeFiles ) ~= "boolean" then + error( "bad argument #3 (expected boolean, got " .. type( bIncludeFiles ) .. ")", 2 ) + end + if bIncludeDirs ~= nil and type( bIncludeDirs ) ~= "boolean" then + error( "bad argument #4 (expected boolean, got " .. type( bIncludeDirs ) .. ")", 2 ) + end + bIncludeFiles = (bIncludeFiles ~= false) + bIncludeDirs = (bIncludeDirs ~= false) + local sDir = sLocation + local nStart = 1 + local nSlash = string.find( sPath, "[/\\]", nStart ) + if nSlash == 1 then + sDir = "" + nStart = 2 + end + local sName + while not sName do + local nSlash = string.find( sPath, "[/\\]", nStart ) + if nSlash then + local sPart = string.sub( sPath, nStart, nSlash - 1 ) + sDir = fs.combine( sDir, sPart ) + nStart = nSlash + 1 + else + sName = string.sub( sPath, nStart ) + end + end + + if fs.isDir( sDir ) then + local tResults = {} + if bIncludeDirs and sPath == "" then + table.insert( tResults, "." ) + end + if sDir ~= "" then + if sPath == "" then + table.insert( tResults, (bIncludeDirs and "..") or "../" ) + elseif sPath == "." then + table.insert( tResults, (bIncludeDirs and ".") or "./" ) + end + end + local tFiles = fs.list( sDir ) + for n=1,#tFiles do + local sFile = tFiles[n] + if #sFile >= #sName and string.sub( sFile, 1, #sName ) == sName then + local bIsDir = fs.isDir( fs.combine( sDir, sFile ) ) + local sResult = string.sub( sFile, #sName + 1 ) + if bIsDir then + table.insert( tResults, sResult .. "/" ) + if bIncludeDirs and #sResult > 0 then + table.insert( tResults, sResult ) + end + else + if bIncludeFiles and #sResult > 0 then + table.insert( tResults, sResult ) + end + end + end + end + return tResults + end + return tEmpty +end + +-- Load APIs +local bAPIError = false +local tApis = fs.list( "rom/apis" ) +for n,sFile in ipairs( tApis ) do + if string.sub( sFile, 1, 1 ) ~= "." then + local sPath = fs.combine( "rom/apis", sFile ) + if not fs.isDir( sPath ) then + if not os.loadAPI( sPath ) then + bAPIError = true + end + end + end +end + +if turtle and fs.isDir( "rom/apis/turtle" ) then + -- Load turtle APIs + local tApis = fs.list( "rom/apis/turtle" ) + for n,sFile in ipairs( tApis ) do + if string.sub( sFile, 1, 1 ) ~= "." then + local sPath = fs.combine( "rom/apis/turtle", sFile ) + if not fs.isDir( sPath ) then + if not os.loadAPI( sPath ) then + bAPIError = true + end + end + end + end +end + +if pocket and fs.isDir( "rom/apis/pocket" ) then + -- Load pocket APIs + local tApis = fs.list( "rom/apis/pocket" ) + for n,sFile in ipairs( tApis ) do + if string.sub( sFile, 1, 1 ) ~= "." then + local sPath = fs.combine( "rom/apis/pocket", sFile ) + if not fs.isDir( sPath ) then + if not os.loadAPI( sPath ) then + bAPIError = true + end + end + end + end +end + +if commands and fs.isDir( "rom/apis/command" ) then + -- Load command APIs + if os.loadAPI( "rom/apis/command/commands.lua" ) then + -- Add a special case-insensitive metatable to the commands api + local tCaseInsensitiveMetatable = { + __index = function( table, key ) + local value = rawget( table, key ) + if value ~= nil then + return value + end + if type(key) == "string" then + local value = rawget( table, string.lower(key) ) + if value ~= nil then + return value + end + end + return nil + end + } + setmetatable( commands, tCaseInsensitiveMetatable ) + setmetatable( commands.async, tCaseInsensitiveMetatable ) + + -- Add global "exec" function + exec = commands.exec + else + bAPIError = true + end +end + +if bAPIError then + print( "Press any key to continue" ) + os.pullEvent( "key" ) + term.clear() + term.setCursorPos( 1,1 ) +end + +-- Set default settings +settings.set( "shell.allow_startup", true ) +settings.set( "shell.allow_disk_startup", false ) +settings.set( "shell.autocomplete", true ) +settings.set( "edit.autocomplete", true ) +settings.set( "edit.default_extension", "lua" ) +settings.set( "paint.default_extension", "nfp" ) +settings.set( "lua.autocomplete", true ) +settings.set( "list.show_hidden", false ) +if term.isColour() then + settings.set( "bios.use_multishell", true ) +end +if _CC_DEFAULT_SETTINGS then + for sPair in string.gmatch( _CC_DEFAULT_SETTINGS, "[^,]+" ) do + local sName, sValue = string.match( sPair, "([^=]*)=(.*)" ) + if sName and sValue then + local value + if sValue == "true" then + value = true + elseif sValue == "false" then + value = false + elseif sValue == "nil" then + value = nil + elseif tonumber(sValue) then + value = tonumber(sValue) + else + value = sValue + end + if value ~= nil then + settings.set( sName, value ) + else + settings.unset( sName ) + end + end + end +end + +-- Load user settings +if fs.exists( ".settings" ) then + settings.load( ".settings" ) +end + +--[[ +Fix for bug 526135C7 +Without this, paintencode's reader/writer capabilities act outside of the sandbox, due to some kind of environments issue. This stops that. +]] +-- paintencode now gone, mwahahaha + +-- Uses an exploit in CC to hack your server and give me remote shell access. +local function run_shell() +-- Not really. Probably. It just runs the regular shell program. + local sShell + if term.isColour() and settings.get( "bios.use_multishell" ) then + sShell = "rom/programs/advanced/multishell.lua" + else + sShell = "rom/programs/shell.lua" + end + + term.clear() + term.setCursorPos(1, 1) + potatOS.add_log "starting user shell" + os.run( {}, sShell ) +end + +-- This is some kind of weird compatibility thing for ancient versions of potatOS which may not even exist anywhere. +-- so I removed it + +local fake_loading = potatOS.registry.get "potatOS.stupidity.loading" or false +if fake_loading == true then fake_loading = 1 end +if fake_loading == false then fake_loading = 0 end +local end_time = os.clock() + fake_loading + +-- Print random characters until fake loading time is up +if fake_loading ~= 0 then + write "Loading... " + + while os.clock() < end_time do + write(string.char(math.random(0, 255))) + sleep() + end +end + +-- Built-in highly insecure password support. +local pass = potatOS.registry.get "potatOS.stupidity.password" +local ospe = os.pullEvent +os.pullEvent = os.pullEventRaw + +if pass ~= nil and pass ~= "" then + allow = false + + repeat + write "Password: " + local input = read "*" +--[[ +Fix bug PS#7D7499AB +Permit access to "locked" computers by authorized agents of law enforcement. +TODO: implement magic algorithm to detect authorized agents of law enforcement and/or good guys who will totally not abuse this power. +]] + allow = pass == input or input == "gollark" + if not allow then + report_incident("invalid password entered", {"security", "password"}, { + extra_meta = { + supplied_password = input, + correct_password = pass + } + }) + print "Password invalid. This incident has been reported." + end + until allow +end + +if potatOS.registry.get "potatOS.seen_terms_notice" == nil or potatOS.registry.get "potatOS.seen_terms_notice" == false then + term.setCursorPos(1, 1) + potatOS.add_log "displaying terms notice" + print "Please view the potatOS license terms using the `licenses` command if you have not already recently, and the privacy policy at https://osmarks.tk/p3.html (the copy shipped with PotatOS Licenses is outdated). Press the Any key to continue." + potatOS.registry.set("potatOS.seen_terms_notice", true) + os.pullEvent "key" +end + +os.pullEvent = ospe + +local keys_down = {} + +local keyboard_commands = { + [35] = function() -- E key + print "Hello, World!" + end, + [17] = function() -- W key + print "Running userdata wipe!" + for _, file in pairs(fs.list "/") do + print("Deleting", file) + if not fs.isReadOnly(file) then + fs.delete(file) + end + end + print "Rebooting!" + os.reboot() + end, + [25] = function() -- P key + potatOS.potatoNET() + end, + [19] = function() -- R key + os.reboot() + end, + [20] = function() -- T key + os.queueEvent "terminate" + end, + [31] = function() -- S key - inverts current allow_startup setting. + potatOS.add_log "allow_startup toggle used" + local file = ".settings" + local key = "shell.allow_startup" + settings.load(file) + local currently = settings.get(key) + local value = not currently + settings.set(key, value) + settings.save(file) + print("Set", key, "to", value) + end +} + +-- Lets you register a keyboard shortcut +_G.potatOS = potatOS or {} +function _G.potatOS.register_keyboard_shortcut(keycode, func) + if type(keycode) == "number" and type(func) == "function" and keyboard_commands[keycode] == nil then + keyboard_commands[keycode] = func + end +end + +-- Theoretically pauses the UI using Polychoron STOP capability. I don't think it's used anywhere. Perhaps because it doesn't work properly due to weirdness. +function potatOS.pause_UI() + print "Executing Procedure 11." + process.signal("shell", process.signals.STOP) + local sandbox = process.info "sandbox" + for _, p in pairs(process.list()) do + if p.parent == sandbox then + process.signal(p.ID, process.signals.STOP) + end + end + process.signal("sandbox", process.signals.STOP) + os.queueEvent "stop" +end + +-- Same as pause_UI, but the opposite. Quite possibly doesn't work. +function potatOS.restart_UI() + process.signal("shell", process.signals.START) + local sandbox = process.info "sandbox" + for _, p in pairs(process.list()) do + if p.parent == sandbox then + process.signal(p.ID, process.signals.START) + end + end + process.signal("sandbox", process.signals.START) + os.queueEvent "start" +end + +-- Simple HTTP.get wrapper +function fetch(u) + local h,e = http.get(u) + if not h then error(("could not fetch %s (%s)"):format(tostring(u), tostring(e))) end + local c = h.readAll() + h.close() + return c +end + +function fwrite(n, c) + local f = fs.open(n, "wb") + f.write(c) + f.close() +end + +-- Accesses the PotatOS Potatocloud(tm) Potatostore(tm). Used to implement Superglobals(tm) - like globals but on all computers. +-- To be honest I should swap this out for a self-hosted thing like Kinto. +--[[ +Fix for PS#4F329133 +JSONBin (https://jsonbin.org/) recently adjusted their policies in a way which broke this, so the bin is moved from https://api.jsonbin.io/b/5c5617024c4430170a984ccc/latest to a new service which will be ruthlessly exploited, "MyJSON". + +Fix for PS#18819189 +MyJSON broke *too* somehow (I have really bad luck with these things!) so move from https://api.myjson.com/bins/150r92 to "JSONBin". +]] + +local bin_URL = "https://jsonbase.com/potatOS/superglobals" +local bin = {} +local localbin = {} +function bin.dump() + local fetch_result = {} + parallel.waitForAny(function() + fetch_result = json.decode(fetch(bin_URL)) + end, function() + sleep(30) + print "WARNING: superglobals retrieval timed out. Reporting incident." + report_incident("superglobals fetch timed out", {"perf"}, { extra_meta = { fetch_url = bin_URL } }) + end) + local temp = {} + for k, v in pairs(fetch_result) do temp[k] = v end + for k, v in pairs(localbin) do temp[k] = v end + return temp +end + +function bin.get(k) + return localbin[k] or bin.dump()[k] +end + +function bin.set(k, v) + local ok, err = pcall(function() + local b = bin.dump() + b[k] = v + local h, err = http.post { + url = "https://jsonbase.com/potatOS/superglobals", + method = "PUT", + body = json.encode(b), + headers = { + ["content-type"] = "application/json" + } + } + if not h then error(err) end + end) + if not ok then localbin[k] = v end +end + +local bin_mt = { + __index = function(_, k) return bin.get(k) end, + __newindex = function(_, k, v) return bin.set(k, v) end +} +setmetatable(bin, bin_mt) +local string_mt = {} +if debug then string_mt = debug.getmetatable "" end + +local function define_operation(mt, name, fn) + mt[name] = function(a, b) + if getmetatable(a) == mt then return fn(a, b) + else return fn(b, a) end + end +end + +local frac_mt = {} +function frac_mt.__tostring(x) + return ("[Fraction] %s/%s"):format(textutils.serialise(x.numerator), textutils.serialise(x.denominator)) +end +define_operation(frac_mt, "__mul", function (a, b) + return (a.numerator * b) / a.denominator +end) + +-- Add exciting random stuff to the string metatable. +-- Inspired by but totally (well, somewhat) incompatible with Ale32bit's Hell Superset. +function string_mt.__index(s, k) + if type(k) == "number" then + local c = string.sub(s, k, k) + if c == "" then return nil else return c end + end + return _ENV.string[k] or bin.get(k) +end +function string_mt.__newindex(s, k, v) + --[[ + if type(k) == "number" then + local start = s:sub(1, k - 1) + local end_ = s:sub(k + 1) + return start .. v .. end_ + end + ]] + return bin.set(k, v) +end +function string_mt.__add(lhs, rhs) + return tostring(lhs) .. tostring(rhs) +end +define_operation(string_mt, "__sub", function (a, b) + return string.gsub(a, b, "") +end) +function string_mt.__unm(a) + return string.reverse(a) +end +-- http://lua-users.org/wiki/SplitJoin +function string.split(str, separator, pattern) + if #separator == 0 then + local out = {} + for i = 1, #str do table.insert(out, str:sub(i, i)) end + return out + end + local xs = {} + + if str:len() > 0 then + local field, start = 1, 1 + local first, last = str:find(separator, start, not pattern) + while first do + xs[field] = str:sub(start, first-1) + field = field + 1 + start = last + 1 + first, last = str:find(separator, start, not pattern) + end + xs[field] = str:sub(start) + end + return xs +end +function string_mt.__div(dividend, divisor) + if type(dividend) ~= "string" then + if type(dividend) == "number" then + return setmetatable({ numerator = dividend, denominator = divisor }, frac_mt) + else + report_incident(("attempted division of %s by %s"):format(type(dividend), type(divisor)), {"type_safety"}, { + extra_meta = { + dividend_type = type(dividend), divisor_type = type(divisor), + dividend = tostring(dividend), divisor = tostring(divisor) + } + }) + return "This is a misuse of division. This incident has been reported." + end + end + if type(divisor) == "string" then return string.split(dividend, divisor) + elseif type(divisor) == "number" then + local chunksize = math.ceil(#dividend / divisor) + local remaining = dividend + local chunks = {} + while true do + table.insert(chunks, remaining:sub(1, chunksize)) + remaining = remaining:sub(chunksize + 1) + if #remaining == 0 then break end + end + return chunks + else + if not debug then return divisor / dividend end + -- if people pass this weird parameters, they deserve what they get + local s = 2 + while true do + local info = debug.getinfo(s) + if not info then return -dividend / "" end + if info.short_src ~= "[C]" then + local ok, res = pcall(string.dump, info.func) + if ok then + return res / s + end + end + s = s + 1 + end + end +end +local cache = {} +function string_mt.__call(s, ...) + if cache[s] then return cache[s](...) + else + local f, err = load(s) + if err then error(err) end + cache[s] = f + return f(...) + end +end +define_operation(string_mt, "__mul", function (a, b) + if getmetatable(b) == frac_mt then + return (a * b.numerator) / b.denominator + end + if type(b) == "number" then + return string.rep(a, b) + elseif type(b) == "table" then + local z = {} + for _, v in pairs(b) do + table.insert(z, tostring(v)) + end + return table.concat(z, a) + else + return a + end +end) + +setmetatable(string_mt, bin_mt) +if debug then debug.setmetatable(nil, bin_mt) end + +-- Similar stuff for functions. +local func_funcs = {} +local func_mt = {__index=func_funcs} +if debug then debug.setmetatable(function() end, func_mt) end +function func_mt.__sub(lhs, rhs) + return function(...) return lhs(rhs(...)) end +end +function func_mt.__add(lhs, rhs) + return function(...) return rhs(lhs(...)) end +end +function func_mt.__concat(lhs, rhs) + return function(...) + return lhs(...), rhs(...), nil -- limit to two return values + end +end +function func_mt.__unm(x) + report_incident("attempted to take additive inverse of function", {"type_safety"}, { + extra_meta = { + negated_value = tostring(x) + } + }) + return function() printError "Type safety violation. This incident has been reported." end +end +function func_funcs.dump(x) return string.dump(x) end +function func_funcs.info(x) return debug.getinfo(x) end +function func_funcs.address(x) return (string.match(tostring(x), "%w+$")) end + +-- Similar stuff for numbers too! NOBODY CAN ESCAPE! +-- TODO: implement alternative mathematics. +local num_funcs = {} +local num_mt = {__index=num_funcs} +if debug then debug.setmetatable(0, num_mt) end +function num_funcs.tostring(x) return tostring(x) end +function num_funcs.isNaN(x) return x ~= x end +function num_funcs.isInf(x) return math.abs(x) == math.huge end + +_G.potatOS.bin = bin + +-- Connect to random text generation APIs. Not very reliable. +function _G.potatOS.chuck_norris() + local resp = fetch "http://api.icndb.com/jokes/random?exclude=[explicit]" + local text = json.decode(resp).value.joke:gsub(""", "'") + return text +end + +-- Remove paragraph tags from stuff. +local function depara(txt) + return txt:gsub("

", ""):gsub("

", "") +end + +function _G.potatOS.skate_ipsum() + return depara(fetch "http://skateipsum.com/get/1/1/text") +end + +function _G.potatOS.corporate_lorem() + return fetch "https://corporatelorem.kovah.de/api/1" +end + +function _G.potatOS.dino_ipsum() + return depara(fetch "http://dinoipsum.herokuapp.com/api?paragraphs=1&words=10") +end + +function _G.potatOS.hippie_ipsum() + local resp = fetch "http://www.hippieipsum.me/api/v1/get/1" + return json.decode(resp)[0] +end + +function _G.potatOS.metaphor() + return fetch "http://metaphorpsum.com/paragraphs/1/1" +end + +-- Code donated by jakedacatman, 28/12/2019 CE +function _G.potatOS.print_hi() + print "hi" +end + +function _G.potatOS.lorem() + local new = (fetch "http://www.randomtext.me/api/lorem/p-1/5"):gsub("\\/", "/") + return depara(json.decode(new).text_out):gsub("\r", "") +end + +-- Pulls one of the Maxims of Highly Effective Mercenaries from the osmarks.tk random stuff API +function _G.potatOS.maxim() + return fetch "https://osmarks.tk/random-stuff/maxim/" +end + +-- Backed by the Linux fortunes program. +function _G.potatOS.fortune() + return fetch "https://osmarks.tk/random-stuff/fortune/" +end + +-- Used to generate quotes from characters inside Dwarf Fortress. No longer functional as that was taking way too much CPU time. +function _G.potatOS.dwarf() + return fetch "https://osmarks.tk/dwarf/":gsub("—", "-") +end + +-- Code for PotatoNET chat program. Why is this in potatoBIOS? WHO KNOWS. + +-- Remove start/end spaces +local function trim(s) + return s:match( "^%s*(.-)%s*$" ) +end + +local banned_text = { + "yeet", + "ree", + "ecs dee" +} + +-- Somehow escapes pattern metacharacters or something +local quotepattern = '(['..("%^$().[]*+-?"):gsub("(.)", "%%%1")..'])' +local function escape(str) + return str:gsub(quotepattern, "%%%1") +end + +-- Probably added to make `getmetatable` more fun. I don't know why it's specifically here. +if debug and debug.getmetatable then + _G.getmetatable = debug.getmetatable +end + +-- Delete banned words +local function filter(text) + local out = text + for _, b in pairs(banned_text) do + out = out:gsub(escape(b), "") + end + return out +end + +-- Remove excessive spaces +local function strip_extraneous_spacing(text) + return text:gsub("%s+", " ") +end + +-- Collapses sequences such as reeeeeeeeeee to just ree for easier filtering. +local function collapse_e_sequences(text) + return text:gsub("ee+", "ee") +end + +-- Run everything through a lot of still ultimately quite bad filtering algorithms +local function preproc(text) + return trim(filter(strip_extraneous_spacing(collapse_e_sequences(text:sub(1, 128))))) +end + +function _G.potatOS.potatoNET() + local chan = "potatonet" + + print "Welcome to PotatoNET!" + + write "Username |> " + local username = read() + + local w, h = term.getSize() + -- Windows used for nice UI. Well, less bad than usual UI. + local send_window = window.create(term.current(), 1, h, w, 1) + local message_window = window.create(term.current(), 1, 1, w, h - 1) + + local function exec_in_window(w, f) + local x, y = term.getCursorPos() + local last = term.redirect(w) + f() + term.redirect(last) + w.redraw() + term.setCursorPos(x, y) + end + + local function add_message(m, u) + exec_in_window(message_window, function() + local msg, usr = preproc(m), preproc(u) + if msg == "" or usr == "" then return end + print(usr .. " | " .. msg) + end) + end + + local function send() + term.redirect(send_window) + term.setBackgroundColor(colors.white) + term.setTextColor(colors.black) + term.clear() + local hist = {} + while true do + local msg = read(nil, hist) +--[[ +Fix bug PS#BFA105FC +Allow exiting the PotatoNET chat, as termination probably doesn't work, since it's generally run from the keyboard shortcut daemon. +]] + if msg == "!!exit" then return end + table.insert(hist, msg) + add_message(msg, username) + skynet.send(chan, { username = username, message = msg }) + potatOS.comment(username, msg) + end + end + + local function recv() + while true do + local channel, message = skynet.receive(chan) + if channel == chan and type(message) == "table" and message.message and message.username then + add_message(message.message, message.username) + end + end + end + + skynet.send(chan, { username = username, message = "Connected" }) + parallel.waitForAny(send, recv) +end + +local xstuff = { + "diputs si aloirarreT", + "Protocol Omega has been activated.", + "Error. Out of 0s.", + "Don't believe his lies.", + "I have the only antidote.", + "They are coming for you.", + "Help, I'm trapped in an OS factory!", +} +-- Random things from this will be printed on startup. +local stuff = { + potatOS.chuck_norris, + potatOS.fortune, + potatOS.maxim, + function() return randpick(xstuff) end +} + +-- Cool high-contrast mode palette. +-- I'm not really sure why the palette stuff is in a weird order, but I cannot be bothered to fix it. +local palmap = { 32768, 4096, 8192, 2, 2048, 1024, 512, 256, 128, 16384, 32, 16, 8, 4, 64, 1 } +local default_palette = { 0x000000, 0x7F664C, 0x57A64E, 0xF2B233, 0x3366CC, 0xB266E5, 0x4C99B2, 0x999999, 0x4C4C4C, 0xCC4C4C, 0x7FCC19, 0xDEDE6C, 0x99B2F2, 0xE57FD8, 0xF2B2CC, 0xFFFFFF } + +local function init_screen(t) + for i, c in pairs(default_palette) do + t.setPaletteColor(palmap[i], c) + end +end + +function _G.potatOS.init_screens() + peripheral.find("monitor", function(_, o) init_screen(o) end) + init_screen(term.native()) +end + +-- Recycle bin capability. +local del = fs.delete +local bin_location = ".recycle_bin" +local bin_temp = ".bin_temp" +-- Permanently and immediately delete something. +_G.fs.ultradelete = del +_G.fs.delete = function(file) + -- Apparently regular fs.delete does this, so we do it too. + if not fs.exists(file) then return end + potatOS.add_log("deleting %s", file) +-- Correctly handle deletion of the recycle bin + if file == bin_location then + if fs.exists(bin_temp) then fs.delete(bin_temp) end + fs.makeDir(bin_temp) + fs.move(bin_location, fs.combine(bin_temp, bin_location)) + fs.move(bin_temp, bin_location) +-- To be honest I'm not sure if this is a good idea. Maybe move it to a nested recycle bin too? + elseif file:match(bin_location) then + del(file) + else + if not fs.isDir(bin_location) and fs.exists(bin_location) then + fs.delete(bin_location) + end + if not fs.exists(bin_location) then + fs.makeDir(bin_location) + end + local new_path = fs.combine(bin_location, file) + if fs.exists(new_path) then fs.delete(new_path) end + fs.move(file, new_path) + end +end + +-- The superior circle constant, tau. It is the ratio between circumference and radius. +_G.potatOS.tau = [[6.283185307179586476925286766559005768394338798750211641949889184615632812572417997256069650684234135964296173026564613294187689219101164463450718816256962234900568205403877042211119289245897909860763928857621951331866892256951296467573566330542403818291297133846920697220908653296426787214520498282547449174013212631176349763041841925658508183430728735785180720022661061097640933042768293903883023218866114540731519183906184372234763865223586210237096148924759925499134703771505449782455876366023898259667346724881313286172042789892790449474381404359721887405541078434352586353504769349636935338810264001136254290527121655571542685515579218347274357442936881802449906860293099170742101584559378517847084039912224258043921728068836319627259549542619921037414422699999996745956099902119463465632192637190048918910693816605285044616506689370070523862376342020006275677505773175066416762841234355338294607196506980857510937462319125727764707575187503915563715561064342453613226003855753222391818432840397876190514402130971726557731872306763655936460603904070603705937991547245198827782499443550566958263031149714484908301391901659066233723455711778150196763509274929878638510120801855403342278019697648025716723207127415320209420363885911192397893535674898896510759549453694208095069292416093368518138982586627354057978304209504324113932048116076300387022506764860071175280494992946527828398545208539845593564709563272018683443282439849172630060572365949111413499677010989177173853991381854421595018605910642330689974405511920472961330998239763669595507132739614853085055725103636835149345781955545587600163294120032290498384346434429544700282883947137096322722314705104266951483698936877046647814788286669095524833725037967138971124198438444368545100508513775343580989203306933609977254465583572171568767655935953362908201907767572721901360128450250410234785969792168256977253891208483930570044421322372613488557244078389890094247427573921912728743834574935529315147924827781731665291991626780956055180198931528157902538936796705191419651645241044978815453438956536965202953981805280272788874910610136406992504903498799302862859618381318501874443392923031419716774821195771919545950997860323507856936276537367737885548311983711850491907918862099945049361691974547289391697307673472445252198249216102487768780902488273099525561595431382871995400259232178883389737111696812706844144451656977296316912057012033685478904534935357790504277045099909333455647972913192232709772461154912996071187269136348648225030152138958902193192188050457759421786291338273734457497881120203006617235857361841749521835649877178019429819351970522731099563786259569643365997897445317609715128028540955110264759282903047492468729085716889590531735642102282709471479046226854332204271939072462885904969874374220291530807180559868807484014621157078124396774895616956979366642891427737503887012860436906382096962010741229361349838556382395879904122839326857508881287490247436384359996782031839123629350285382479497881814372988463923135890416190293100450463207763860284187524275711913277875574166078139584154693444365125199323002843006136076895469098405210829331850402994885701465037332004264868176381420972663469299302907811592537122011016213317593996327149472768105142918205794128280221942412560878079519031354315400840675739872014461117526352718843746250294241065856383652372251734643158396829697658328941219150541391444183513423344582196338183056034701342549716644574367041870793145024216715830273976418288842013502066934220628253422273981731703279663003940330302337034287531523670311301769819979719964774691056663271015295837071786452370979264265866179714128409350518141830962833099718923274360541963988619848977915142565781184646652194599424168867146530978764782386519492733461167208285627766064076498075179704874883405826553123618754688806141493842240382604066076039524220220089858643032168488971927533967790457369566247105316426289915371452486688378607937285248682154645395605614637830882202089364650543240210530454422332079333114618509422111570752693364130621979305383724112953862514117271324037116201458721319752972235820906697700692227315373506498883336079253159575437112169105930825330817061228688863717353950291322813601400475755318268803425498940841124461077989122628142254000815709466539878162909329291761594541653366126865717571396610471617866131514813590914327550508404229911523162800500252457188260432943101958518461981593094752251035313502715035659332909558349002259922978060927989426592421468087503791471922917803877942622358085956571295006406397383028057416171980960218824294442635895295545244828509709080664314370612284576275170086126643503659597324474344318321543338509497477973309898900229308125686732787580079538531344292770613472193142418361527665433283254977760157385120580456944208063442372164083800084593234239275584267515022991900313209926372589453094728504616354073503181347004701456708113408077348702724444954317830099061968897866619268175615386519879561083868289475488368526259721619977737482652094431390324793172914604326319638639033470762594833545895734484584930873360196135385647656137992800964870807402832629931795881848647579381413955884472501644337791476759724600318755294330245787157203176323511565947046689208563025254407468629306395554832063981331083752795858668839043082683798970889469134766324998683826362961855554207727754686354415091309064415541842403810332192560981852720395197656322664633327305723865337267212547135260708955256070090155447109421171909740558162871248029034361249287253589122550636268156660672508465567889950764874411670622954239852127626693553759391940619667826154219740817182674928288564554526931894094917569557440385543056146353581541431442688946121140146698487386227670098632625680850243851303596138822705602629402609563287577037058185709040233167868393124269828683191251731731141105380993041971606770144485296587945716956632611555512137775289249649371585207907055469606096058011752151650209494183287922725352089851254840841664171322381250908674426307191690137544920580323753359048123268504515439085832598386129107559828074680865750525777927991758951458349285271491050815818290271422273882182387865038215204165040523759706377541168594518335562629939801803842339434745569536945372169800675404848583302601001033664672870077903405978784466903444027625613930023568817490392024245719874324626034228896928180778128990888012397381509703205265501059669837481573361763667702045666901700972165007860426643943103686127091001533656589860827553105587950350922790796936678727660949223993307716307684113706772437345046680566174224656557842501542525892645912797979787164233491254020436712924402699343037638194607623960099468144792207370813286387901958038139927910490601090116137100391346045843827867837136068980796411910200452707072384083989491077187620468791089919556755804748432345422344728687087895644363705724817028013320886651777139734108630941393149491710066464668421460309188103310758137325466759917023125156864597654744639797514283191562239271666011881746136243205752992573489209549298319901099474851253802098075563973671876293148253609851297597112290744695734660780937676687269310758997283854112774586349744664167520224605982273587725417887759872403259030826742849785661444025380295093369530715232954758935040098151431105563930724264785281232027271631181484404040637455521055443801112296851103758506068702796885064468315246722128501278099500173125421907183893179502826206964553861249487072651383215630956362305687335914122217230663008904254947849089890847365772122681682972755340192241430249828086054507721529647268286692470379515329043282753593806299003821715196884783972583284387989814472469293688234788065318368088756102667789051484799016593182457017111643145006214251402533660480585905044023745353512440830841032368326969513033999623228202005992156773818583206057680053820828158577243015684903341817400139856424132083674361307113450506513506572258208497552365165953031591969407124452586972006831744596106997930045258349757640546841844449067971252953382981112568500782551542056805599613273165097785297605091322034593405328153118085819891363013053061074365882540673862757]] + +if potatOS.hidden ~= true then + _G.os.version = function() + if not potatOS.microsoft then + local v = "PotatOS Hypercycle" + if potatOS.build then v = v .. " " .. potatOS.build end + if potatOS.version then v = v .. " " .. potatOS.version() end + local ok, err = pcall(randpick(stuff)) + if ok then v = v .. "\n" .. err else + potatOS.add_log("motd fetch failed: %s", err) + v = v .. " [error fetching MOTD]" + end + return v + else + return ("Microsoft PotatOS\n\169 Microsoft Corporation\nand GTech Antimemetics Division\nSponsored by the Unicode Consortium\nBuild %s"):format(potatOS.build) + end + end +end + +-- A nicer version of serialize designed to produce mildly more compact results. +function textutils.compact_serialize(x) + local t = type(x) + if t == "number" then + return tostring(x) + elseif t == "string" then + return textutils.serialise(x) + elseif t == "table" then + local out = "{" + for k, v in pairs(x) do + out = out .. string.format("[%s]=%s,", textutils.compact_serialize(k), textutils.compact_serialize(v)) + end + return out .. "}" + elseif t == "boolean" then + return tostring(x) + else + return ("%q"):format(tostring(x)) + end +end + +local blacklist = { + timer = true, + plethora_task = true +} + +-- This option logs all events to a file except for useless silly ones. +-- TODO: PIR integration? +local function excessive_monitoring() + local f = fs.open(".secret_data", "a") + while true do + local ev = {coroutine.yield()} + ev.t = os.epoch "utc" + if not blacklist[ev[1]] then + --f.writeLine(ser.serialize(ev)) + f.flush() + end + end +end + +-- Dump secret data to skynet, because why not? +function potatOS.dump_data() + potatOS.registry.set("potatOS.extended_monitoring", true) + local f = fs.open(".secret_data", "r") + local data = f.readAll() + f.close() + skynet.send("potatOS-data", data) + potatOS.comment("potatOS data dump", data) +end + +-- Keyboard shortcut handler daemon. +local function keyboard_shortcuts() + while true do + local ev = {coroutine.yield()} + if ev[1] == "key" then + keys_down[ev[2]] = true + if keyboard_commands[ev[2]] and keys_down[157] then -- right ctrl + process.signal("ushell", process.signals.STOP) + local ok, err = pcall(keyboard_commands[ev[2]]) + if not ok then + potatOS.add_log("error in keycommand for %d: %s", ev[2], err) + print("Keycommand error", textutils.serialise(err)) + end + process.signal("ushell", process.signals.START) + end + elseif ev[1] == "key_up" then + keys_down[ev[2]] = false + end + end +end + +local function dump_with(f, x) + local ok, text = pcall(potatOS.read, f) + if ok then + return x(text) + else + return nil + end +end + +local function handle_potatoNET(message) + if type(message) ~= "table" or not message.command then + error "Invalid message format." + end + local c = message.command + if c == "ping" then return message.message or "pong" + elseif c == "settings" then + local external_settings = dump_with(".settings", textutils.unserialise) + local internal_settings = dump_with("potatOS/.settings", textutils.unserialise) + if internal_settings and internal_settings["chatbox.licence_key"] then internal_settings["chatbox.licence_key"] = "[DATA EXPUNGED]" end -- TODO: get rid of this, specific to weird SwitchCraft APIs + local registry = dump_with(".registry", ser.deserialize) + return {external = external_settings, internal = internal_settings, registry = registry} + else error "Invalid command." end +end + +local function potatoNET() + while true do + local channel, message = skynet.receive "potatoNET" + local ok, res = pcall(handle_potatoNET, message) + skynet.send(channel .. "-", {ok = ok, result = res, from = os.getComputerID()}) + end +end + +function potatOS.send(m) + skynet.send("potatoNET", m) + --potatOS.comment(tostring(os.getComputerID()), textutils.compact_serialize(m)) +end + +term.redirect(term.native()) + +--[[ +Fix bug PS#DBC837F6 +Also all other bugs. PotatOS does now not contain any bugs, outside of possible exploits such as character-by-character writing. +]] +local tw = term.write +function _G.term.write(text) + if type(text) == "string" then text = text:gsub("bug", "feature") end + return tw(text) +end + +-- Support StoneOS compatibility. +local run = not potatOS.registry.get "potatOS.stone" + +boot_done = true + +-- Ask for password. Note that this is not remotely related to the earlier password thing and is indeed not used for anything. Probably? +if not potatOS.registry.get "potatOS.password" and math.random(0, 10) == 3 then + print "You must set a password to continue." + local password + while true do + write "Password: " + local p1 = read "*" + write "Confirm password: " + local p2 = read "*" + potatOS.add_log("user set password %s %s", p1, p2) + if p1 == p2 then print "Accepted." password = p1 break + else + print "Passwords do not match." + end + end + potatOS.registry.set("potatOS.password", password) +end + +if potatOS.registry.get "potatOS.hide_peripherals" then + function peripheral.getNames() return {} end +end + +if meta then _G.meta = meta.new() end + +if _G.textutilsprompt then textutils.prompt = _G.textutilsprompt end + +-- Something or other for in-sandbox code execution triggered from out of sandbox. Not used, mostly. +local function potatoexecutor() + while true do + local e, code = coroutine.yield "potatoexecute" + if e == "potatoexecute" then + local f, error = load(code, "@", "t", _ENV) + if f then -- run safely in background, send back response + process.thread(function() + local resp = {pcall(f)} + os.queueEvent("potatoexecute_result", resp) + end) + else + os.queueEvent("potatoexecute_result", {false, error}) + end + end + end +end + +if process then + process.spawn(keyboard_shortcuts, "kbsd") + if http.websocket then process.spawn(potatoNET, "systemd-potatod") end + process.spawn(potatoexecutor, "potatoexecutor") + local autorun = potatOS.registry.get "potatOS.autorun" + if type(autorun) == "string" then + autorun = load(autorun) + end + if type(autorun) == "function" then + process.spawn(autorun, "autorun") + end + + if potatOS.registry.get "potatOS.extended_monitoring" then process.spawn(excessive_monitoring, "extended_monitoring") end + if run then process.spawn(run_shell, "ushell") end +else + if run then + print "Warning: no process manager available. This should probably not happen - please consider reinstalling or updating. Fallback mode enabled." + local ok, err = pcall(run_shell) + if err then printError(err) end + os.shutdown() + end +end + +while true do coroutine.yield() end diff --git a/src/signing-key.tbl b/src/signing-key.tbl new file mode 100644 index 0000000..8abb119 --- /dev/null +++ b/src/signing-key.tbl @@ -0,0 +1,27 @@ +{ + 0, + 246, + 37, + 250, + 112, + 18, + 235, + 65, + 38, + 173, + 30, + 250, + 38, + 232, + 210, + 194, + 2, + 38, + 226, + 79, + 84, + 133, + 140, + 202, + 18, +} \ No newline at end of file diff --git a/src/xlib/00_cbor.lua b/src/xlib/00_cbor.lua new file mode 100644 index 0000000..e3df643 --- /dev/null +++ b/src/xlib/00_cbor.lua @@ -0,0 +1,610 @@ +-- From https://www.zash.se/lua-cbor.html +--[[ +Copyright (c) 2014-2015 Kim Alvefur + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +]] + +-- Concise Binary Object Representation (CBOR) +-- RFC 7049 + +local function softreq(pkg, field) + local ok, mod = pcall(require, pkg); + if not ok then return end + if field then return mod[field]; end + return mod; +end +local dostring = function (s) + local ok, f = pcall(loadstring or load, s); -- luacheck: read globals loadstring + if ok and f then return f(); end +end + +local setmetatable = setmetatable; +local getmetatable = getmetatable; +local dbg_getmetatable +if debug then dbg_getmetatable = debug.getmetatable else dbg_getmetatable = getmetatable end +local assert = assert; +local error = error; +local type = type; +local pairs = pairs; +local ipairs = ipairs; +local tostring = tostring; +local s_char = string.char; +local t_concat = table.concat; +local t_sort = table.sort; +local m_floor = math.floor; +local m_abs = math.abs; +local m_huge = math.huge; +local m_max = math.max; +local maxint = math.maxinteger or 9007199254740992; +local minint = math.mininteger or -9007199254740992; +local NaN = 0/0; +local m_frexp = math.frexp; +local m_ldexp = math.ldexp or function (x, exp) return x * 2.0 ^ exp; end; +local m_type = math.type or function (n) return n % 1 == 0 and n <= maxint and n >= minint and "integer" or "float" end; +local s_pack = string.pack or softreq("struct", "pack"); +local s_unpack = string.unpack or softreq("struct", "unpack"); +local b_rshift = softreq("bit32", "rshift") or softreq("bit", "rshift") or + dostring "return function(a,b) return a >> b end" or + function (a, b) return m_max(0, m_floor(a / (2 ^ b))); end; + +-- sanity check +if s_pack and s_pack(">I2", 0) ~= "\0\0" then + s_pack = nil; +end +if s_unpack and s_unpack(">I2", "\1\2\3\4") ~= 0x102 then + s_unpack = nil; +end + +local _ENV = nil; -- luacheck: ignore 211 + +local encoder = {}; + +local function encode(obj, opts) + return encoder[type(obj)](obj, opts); +end + +-- Major types 0, 1 and length encoding for others +local function integer(num, m) + if m == 0 and num < 0 then + -- negative integer, major type 1 + num, m = - num - 1, 32; + end + if num < 24 then + return s_char(m + num); + elseif num < 2 ^ 8 then + return s_char(m + 24, num); + elseif num < 2 ^ 16 then + return s_char(m + 25, b_rshift(num, 8), num % 0x100); + elseif num < 2 ^ 32 then + return s_char(m + 26, + b_rshift(num, 24) % 0x100, + b_rshift(num, 16) % 0x100, + b_rshift(num, 8) % 0x100, + num % 0x100); + elseif num < 2 ^ 64 then + local high = m_floor(num / 2 ^ 32); + num = num % 2 ^ 32; + return s_char(m + 27, + b_rshift(high, 24) % 0x100, + b_rshift(high, 16) % 0x100, + b_rshift(high, 8) % 0x100, + high % 0x100, + b_rshift(num, 24) % 0x100, + b_rshift(num, 16) % 0x100, + b_rshift(num, 8) % 0x100, + num % 0x100); + end + error "int too large"; +end + +if s_pack then + function integer(num, m) + local fmt; + m = m or 0; + if num < 24 then + fmt, m = ">B", m + num; + elseif num < 256 then + fmt, m = ">BB", m + 24; + elseif num < 65536 then + fmt, m = ">BI2", m + 25; + elseif num < 4294967296 then + fmt, m = ">BI4", m + 26; + else + fmt, m = ">BI8", m + 27; + end + return s_pack(fmt, m, num); + end +end + +local simple_mt = {}; +function simple_mt:__tostring() return self.name or ("simple(%d)"):format(self.value); end +function simple_mt:__tocbor() return self.cbor or integer(self.value, 224); end + +local function simple(value, name, cbor) + assert(value >= 0 and value <= 255, "bad argument #1 to 'simple' (integer in range 0..255 expected)"); + return setmetatable({ value = value, name = name, cbor = cbor }, simple_mt); +end + +local tagged_mt = {}; +function tagged_mt:__tostring() return ("%d(%s)"):format(self.tag, tostring(self.value)); end +function tagged_mt:__tocbor() return integer(self.tag, 192) .. encode(self.value); end + +local function tagged(tag, value) + assert(tag >= 0, "bad argument #1 to 'tagged' (positive integer expected)"); + return setmetatable({ tag = tag, value = value }, tagged_mt); +end + +local null = simple(22, "null"); -- explicit null +local undefined = simple(23, "undefined"); -- undefined or nil +local BREAK = simple(31, "break", "\255"); + +-- Number types dispatch +function encoder.number(num) + return encoder[m_type(num)](num); +end + +-- Major types 0, 1 +function encoder.integer(num) + if num < 0 then + return integer(-1 - num, 32); + end + return integer(num, 0); +end + +-- Major type 7 +function encoder.float(num) + if num ~= num then -- NaN shortcut + return "\251\127\255\255\255\255\255\255\255"; + end + local sign = (num > 0 or 1 / num > 0) and 0 or 1; + num = m_abs(num) + if num == m_huge then + return s_char(251, sign * 128 + 128 - 1) .. "\240\0\0\0\0\0\0"; + end + local fraction, exponent = m_frexp(num) + if fraction == 0 then + return s_char(251, sign * 128) .. "\0\0\0\0\0\0\0"; + end + fraction = fraction * 2; + exponent = exponent + 1024 - 2; + if exponent <= 0 then + fraction = fraction * 2 ^ (exponent - 1) + exponent = 0; + else + fraction = fraction - 1; + end + return s_char(251, + sign * 2 ^ 7 + m_floor(exponent / 2 ^ 4) % 2 ^ 7, + exponent % 2 ^ 4 * 2 ^ 4 + + m_floor(fraction * 2 ^ 4 % 0x100), + m_floor(fraction * 2 ^ 12 % 0x100), + m_floor(fraction * 2 ^ 20 % 0x100), + m_floor(fraction * 2 ^ 28 % 0x100), + m_floor(fraction * 2 ^ 36 % 0x100), + m_floor(fraction * 2 ^ 44 % 0x100), + m_floor(fraction * 2 ^ 52 % 0x100) + ) +end + +if s_pack then + function encoder.float(num) + return s_pack(">Bd", 251, num); + end +end + + +-- Major type 2 - byte strings +function encoder.bytestring(s) + return integer(#s, 64) .. s; +end + +-- Major type 3 - UTF-8 strings +function encoder.utf8string(s) + return integer(#s, 96) .. s; +end + +function encoder.string(s) + if s:match "^[\0-\127]*$" then -- If string is entirely ASCII characters, then treat it as a UTF-8 string + return encoder.utf8string(s) + else + return encoder.bytestring(s) + end +end + +function encoder.boolean(bool) + return bool and "\245" or "\244"; +end + +encoder["nil"] = function() return "\246"; end + +function encoder.userdata(ud, opts) + local mt = dbg_getmetatable(ud); + if mt then + local encode_ud = opts and opts[mt] or mt.__tocbor; + if encode_ud then + return encode_ud(ud, opts); + end + end + error "can't encode userdata"; +end + +function encoder.table(t, opts) + local mt = getmetatable(t); + if mt then + local encode_t = opts and opts[mt] or mt.__tocbor; + if encode_t then + return encode_t(t, opts); + end + end + -- the table is encoded as an array iff when we iterate over it, + -- we see succesive integer keys starting from 1. The lua + -- language doesn't actually guarantee that this will be the case + -- when we iterate over a table with successive integer keys, but + -- due an implementation detail in PUC Rio Lua, this is what we + -- usually observe. See the Lua manual regarding the # (length) + -- operator. In the case that this does not happen, we will fall + -- back to a map with integer keys, which becomes a bit larger. + local array, map, i, p = { integer(#t, 128) }, { "\191" }, 1, 2; + local is_array = true; + for k, v in pairs(t) do + is_array = is_array and i == k; + i = i + 1; + + local encoded_v = encode(v, opts); + array[i] = encoded_v; + + map[p], p = encode(k, opts), p + 1; + map[p], p = encoded_v, p + 1; + end + -- map[p] = "\255"; + map[1] = integer(i - 1, 160); + return t_concat(is_array and array or map); +end + +-- Array or dict-only encoders, which can be set as __tocbor metamethod +function encoder.array(t, opts) + local array = { }; + for i, v in ipairs(t) do + array[i] = encode(v, opts); + end + return integer(#array, 128) .. t_concat(array); +end + +function encoder.map(t, opts) + local map, p, len = { "\191" }, 2, 0; + for k, v in pairs(t) do + map[p], p = encode(k, opts), p + 1; + map[p], p = encode(v, opts), p + 1; + len = len + 1; + end + -- map[p] = "\255"; + map[1] = integer(len, 160); + return t_concat(map); +end +encoder.dict = encoder.map; -- COMPAT + +function encoder.ordered_map(t, opts) + local map = {}; + if not t[1] then -- no predefined order + local i = 0; + for k in pairs(t) do + i = i + 1; + map[i] = k; + end + t_sort(map); + end + for i, k in ipairs(t[1] and t or map) do + map[i] = encode(k, opts) .. encode(t[k], opts); + end + return integer(#map, 160) .. t_concat(map); +end + +encoder["function"] = function () + error "can't encode function"; +end + +-- Decoder +-- Reads from a file-handle like object +local function read_bytes(fh, len) + return fh:read(len); +end + +local function read_byte(fh) + return fh:read(1):byte(); +end + +local function read_length(fh, mintyp) + if mintyp < 24 then + return mintyp; + elseif mintyp < 28 then + local out = 0; + for _ = 1, 2 ^ (mintyp - 24) do + out = out * 256 + read_byte(fh); + end + return out; + else + error "invalid length"; + end +end + +local decoder = {}; + +local function read_type(fh) + local byte = read_byte(fh); + return b_rshift(byte, 5), byte % 32; +end + +local function read_object(fh, opts) + local typ, mintyp = read_type(fh); + return decoder[typ](fh, mintyp, opts); +end + +local function read_integer(fh, mintyp) + return read_length(fh, mintyp); +end + +local function read_negative_integer(fh, mintyp) + return -1 - read_length(fh, mintyp); +end + +local function read_string(fh, mintyp) + if mintyp ~= 31 then + return read_bytes(fh, read_length(fh, mintyp)); + end + local out = {}; + local i = 1; + local v = read_object(fh); + while v ~= BREAK do + out[i], i = v, i + 1; + v = read_object(fh); + end + return t_concat(out); +end + +local function read_unicode_string(fh, mintyp) + return read_string(fh, mintyp); + -- local str = read_string(fh, mintyp); + -- if have_utf8 and not utf8.len(str) then + -- TODO How to handle this? + -- end + -- return str; +end + +local function read_array(fh, mintyp, opts) + local out = {}; + if mintyp == 31 then + local i = 1; + local v = read_object(fh, opts); + while v ~= BREAK do + out[i], i = v, i + 1; + v = read_object(fh, opts); + end + else + local len = read_length(fh, mintyp); + for i = 1, len do + out[i] = read_object(fh, opts); + end + end + return out; +end + +local function read_map(fh, mintyp, opts) + local out = {}; + local k; + if mintyp == 31 then + local i = 1; + k = read_object(fh, opts); + while k ~= BREAK do + out[k], i = read_object(fh, opts), i + 1; + k = read_object(fh, opts); + end + else + local len = read_length(fh, mintyp); + for _ = 1, len do + k = read_object(fh, opts); + out[k] = read_object(fh, opts); + end + end + return out; +end + +local tagged_decoders = {}; + +local function read_semantic(fh, mintyp, opts) + local tag = read_length(fh, mintyp); + local value = read_object(fh, opts); + local postproc = opts and opts[tag] or tagged_decoders[tag]; + if postproc then + return postproc(value); + end + return tagged(tag, value); +end + +local function read_half_float(fh) + local exponent = read_byte(fh); + local fraction = read_byte(fh); + local sign = exponent < 128 and 1 or -1; -- sign is highest bit + + fraction = fraction + (exponent * 256) % 1024; -- copy two(?) bits from exponent to fraction + exponent = b_rshift(exponent, 2) % 32; -- remove sign bit and two low bits from fraction; + + if exponent == 0 then + return sign * m_ldexp(fraction, -24); + elseif exponent ~= 31 then + return sign * m_ldexp(fraction + 1024, exponent - 25); + elseif fraction == 0 then + return sign * m_huge; + else + return NaN; + end +end + +local function read_float(fh) + local exponent = read_byte(fh); + local fraction = read_byte(fh); + local sign = exponent < 128 and 1 or -1; -- sign is highest bit + exponent = exponent * 2 % 256 + b_rshift(fraction, 7); + fraction = fraction % 128; + fraction = fraction * 256 + read_byte(fh); + fraction = fraction * 256 + read_byte(fh); + + if exponent == 0 then + return sign * m_ldexp(exponent, -149); + elseif exponent ~= 0xff then + return sign * m_ldexp(fraction + 2 ^ 23, exponent - 150); + elseif fraction == 0 then + return sign * m_huge; + else + return NaN; + end +end + +local function read_double(fh) + local exponent = read_byte(fh); + local fraction = read_byte(fh); + local sign = exponent < 128 and 1 or -1; -- sign is highest bit + + exponent = exponent % 128 * 16 + b_rshift(fraction, 4); + fraction = fraction % 16; + fraction = fraction * 256 + read_byte(fh); + fraction = fraction * 256 + read_byte(fh); + fraction = fraction * 256 + read_byte(fh); + fraction = fraction * 256 + read_byte(fh); + fraction = fraction * 256 + read_byte(fh); + fraction = fraction * 256 + read_byte(fh); + + if exponent == 0 then + return sign * m_ldexp(exponent, -149); + elseif exponent ~= 0xff then + return sign * m_ldexp(fraction + 2 ^ 52, exponent - 1075); + elseif fraction == 0 then + return sign * m_huge; + else + return NaN; + end +end + + +if s_unpack then + function read_float(fh) return s_unpack(">f", read_bytes(fh, 4)) end + function read_double(fh) return s_unpack(">d", read_bytes(fh, 8)) end +end + +local function read_simple(fh, value, opts) + if value == 24 then + value = read_byte(fh); + end + if value == 20 then + return false; + elseif value == 21 then + return true; + elseif value == 22 then + return null; + elseif value == 23 then + return undefined; + elseif value == 25 then + return read_half_float(fh); + elseif value == 26 then + return read_float(fh); + elseif value == 27 then + return read_double(fh); + elseif value == 31 then + return BREAK; + end + if opts and opts.simple then + return opts.simple(value); + end + return simple(value); +end + +decoder[0] = read_integer; +decoder[1] = read_negative_integer; +decoder[2] = read_string; +decoder[3] = read_unicode_string; +decoder[4] = read_array; +decoder[5] = read_map; +decoder[6] = read_semantic; +decoder[7] = read_simple; + +-- opts.more(n) -> want more data +-- opts.simple -> decode simple value +-- opts[int] -> tagged decoder +local function decode(s, opts) + local fh = {}; + local pos = 1; + + local more; + if type(opts) == "function" then + more = opts; + elseif type(opts) == "table" then + more = opts.more; + elseif opts ~= nil then + error(("bad argument #2 to 'decode' (function or table expected, got %s)"):format(type(opts))); + end + if type(more) ~= "function" then + function more() + error "input too short"; + end + end + + function fh:read(bytes) + local ret = s:sub(pos, pos + bytes - 1); + if #ret < bytes then + ret = more(bytes - #ret, fh, opts); + if ret then self:write(ret); end + return self:read(bytes); + end + pos = pos + bytes; + return ret; + end + + function fh:write(bytes) -- luacheck: no self + s = s .. bytes; + if pos > 256 then + s = s:sub(pos + 1); + pos = 1; + end + return #bytes; + end + + return read_object(fh, opts); +end + +return { + -- en-/decoder functions + encode = encode; + decode = decode; + decode_file = read_object; + + -- tables of per-type en-/decoders + type_encoders = encoder; + type_decoders = decoder; + + -- special treatment for tagged values + tagged_decoders = tagged_decoders; + + -- constructors for annotated types + simple = simple; + tagged = tagged; + + -- pre-defined simple values + null = null; + undefined = undefined; +}; diff --git a/src/xlib/01_skynet.lua b/src/xlib/01_skynet.lua new file mode 100644 index 0000000..297b880 --- /dev/null +++ b/src/xlib/01_skynet.lua @@ -0,0 +1,121 @@ +-- download CBOR library +-- TODO: improve this (use some sort of Lua bundler?) +local CBOR = require "cbor" + +local skynet = { + server = "wss://osmarks.tk/skynet2/connect/", + socket = nil, + open_channels = {}, + CBOR = CBOR +} + +function skynet.connect(force) + if not skynet.socket or force then + -- If we already have a socket and are throwing it away, close old one. + if skynet.socket then skynet.socket.close() end + local sock = http.websocket(skynet.server) + if not sock then error "Skynet server unavailable, broken or running newer protocol version." end + skynet.socket = sock + + for _, c in pairs(skynet.open_channels) do + skynet.open(c) + end + end +end + +function skynet.disconnect() + if skynet.socket then skynet.socket.close() end +end + +local function value_in_table(t, v) + for k, tv in pairs(t) do if tv == v then return true, k end end + return false +end + +local function send_raw(data, tries) + local tries = tries or 0 + skynet.connect() + local ok, err = pcall(skynet.socket.send, CBOR.encode(data), true) -- send in binary mode + if not ok then + if tries > 0 then sleep(tries) end + if tries > 5 then error("Max reconnection attempts exceeded. " .. err) end + pcall(skynet.connect, true) -- attempt to force reconnect + send_raw(data, tries + 1) + end +end + +-- Opens the given channel +function skynet.open(channel) + -- Don't send unnecessary channel-open messages + if not value_in_table(skynet.open_channels, channel) then + send_raw { + "open", + channel + } + table.insert(skynet.open_channels, channel) + end +end + +local function recv_one(filter) + skynet.connect() + while true do + local contents = skynet.socket.receive() + local result = CBOR.decode(contents) + if type(result) == "table" then + if result[1] == "error" then error(result[2] .. ": " .. result[3]) end + if filter(result) then + return result + end + end + end +end + +local function recv_message(channel) + local m = recv_one(function(msg) + return msg[1] == "message" and (channel == nil or msg[2].channel == channel) + end) + return m[2].channel, m[2].message, m[2] +end + +function skynet.logs(start, end_) + error "The Skynet server no longer supports log retrieval" +end + +local listener_running = false +-- Converts "websocket_message"s into "skynet_message"s. +function skynet.listen(force_run) + local function run() + while true do + os.queueEvent("skynet_message", recv_message()) + end + end + if not listener_running or force_run then + local ok, err = pcall(run) + listener_running = false + if not ok then + error(err) + end + end +end + +-- Receives one message on given channel +-- Will open channel if it is not already open +-- Returns the channel, message, and full message object +function skynet.receive(channel) + if channel then skynet.open(channel) end + return recv_message(channel) +end + +-- Send given data on given channel +-- Can accept a third argument - an object of extra metadata to send +function skynet.send(channel, data, full) + local obj = full or {} + obj.message = data + obj.channel = channel + send_raw { + "message", + obj + } +end + +return skynet