shironeko/api.lua

by hugeblank
155 days agolua
COPY
1
local version = 022
2
local expect = require("cc.expect")
3
local cha = (function()
4
    -- Chacha20 cipher in ComputerCraft
5
    -- By Anavrins
6
    -- For help and details, you can DM me on Discord (Anavrins#4600)
7
    -- MIT License
8
    -- Pastebin: https://pastebin.com/GPzf9JSa
9
    -- Last updated: March 27 2020
10

11
    local mod32 = 2^32
12
    local bor = bit32.bor
13
    local bxor = bit32.bxor
14
    local band = bit32.band
15
    local blshift = bit32.lshift
16
    local brshift = bit32.arshift
17

18
    local tau = {("expand 16-byte k"):byte(1,-1)}
19
    local sigma = {("expand 32-byte k"):byte(1,-1)}
20
    local null32 = {("A"):rep(32):byte(1,-1)}
21
    local null12 = {("A"):rep(12):byte(1,-1)}
22

23
    local function rotl(n, b)
24
        local s = n/(2^(32-b))
25
        local f = s%1
26
        return (s-f) + f*mod32
27
    end
28

29
    local function quarterRound(s, a, b, c, d)
30
        s[a] = (s[a]+s[b])%mod32; s[d] = rotl(bxor(s[d], s[a]), 16)
31
        s[c] = (s[c]+s[d])%mod32; s[b] = rotl(bxor(s[b], s[c]), 12)
32
        s[a] = (s[a]+s[b])%mod32; s[d] = rotl(bxor(s[d], s[a]), 8)
33
        s[c] = (s[c]+s[d])%mod32; s[b] = rotl(bxor(s[b], s[c]), 7)
34
        return s
35
    end
36

37
    local function hashBlock(state, rnd)
38
        local s = {unpack(state)}
39
        for i = 1, rnd do
40
            local r = i%2==1
41
            s = r and quarterRound(s, 1, 5,  9, 13) or quarterRound(s, 1, 6, 11, 16)
42
            s = r and quarterRound(s, 2, 6, 10, 14) or quarterRound(s, 2, 7, 12, 13)
43
            s = r and quarterRound(s, 3, 7, 11, 15) or quarterRound(s, 3, 8,  9, 14)
44
            s = r and quarterRound(s, 4, 8, 12, 16) or quarterRound(s, 4, 5, 10, 15)
45
        end
46
        for i = 1, 16 do s[i] = (s[i]+state[i])%mod32 end
47
        return s
48
    end
49

50
    local function LE_toInt(bs, i)
51
        return (bs[i+1] or 0)+
52
        blshift((bs[i+2] or 0), 8)+
53
        blshift((bs[i+3] or 0), 16)+
54
        blshift((bs[i+4] or 0), 24)
55
    end
56

57
    local function initState(key, nonce, counter)
58
        local isKey256 = #key == 32
59
        local const = isKey256 and sigma or tau
60
        local state = {}
61

62
        state[ 1] = LE_toInt(const, 0)
63
        state[ 2] = LE_toInt(const, 4)
64
        state[ 3] = LE_toInt(const, 8)
65
        state[ 4] = LE_toInt(const, 12)
66

67
        state[ 5] = LE_toInt(key, 0)
68
        state[ 6] = LE_toInt(key, 4)
69
        state[ 7] = LE_toInt(key, 8)
70
        state[ 8] = LE_toInt(key, 12)
71
        state[ 9] = LE_toInt(key, isKey256 and 16 or 0)
72
        state[10] = LE_toInt(key, isKey256 and 20 or 4)
73
        state[11] = LE_toInt(key, isKey256 and 24 or 8)
74
        state[12] = LE_toInt(key, isKey256 and 28 or 12)
75

76
        state[13] = counter
77
        state[14] = LE_toInt(nonce, 0)
78
        state[15] = LE_toInt(nonce, 4)
79
        state[16] = LE_toInt(nonce, 8)
80

81
        return state
82
    end
83

84
    local function serialize(state)
85
        local r = {}
86
        for i = 1, 16 do
87
            r[#r+1] = band(state[i], 0xFF)
88
            r[#r+1] = band(brshift(state[i], 8), 0xFF)
89
            r[#r+1] = band(brshift(state[i], 16), 0xFF)
90
            r[#r+1] = band(brshift(state[i], 24), 0xFF)
91
        end
92
        return r
93
    end
94

95
    local mt = {
96
        __tostring = function(a) return string.char(unpack(a)) end,
97
        __index = {
98
            toHex = function(self) return ("%02x"):rep(#self):format(unpack(self)) end,
99
            isEqual = function(self, t)
100
                if type(t) ~= "table" then return false end
101
                if #self ~= #t then return false end
102
                local ret = 0
103
                for i = 1, #self do
104
                    ret = bor(ret, bxor(self[i], t[i]))
105
                end
106
                return ret == 0
107
            end
108
        }
109
    }
110

111
    local function crypt(data, key, nonce, cntr, round)
112
        assert(type(key) == "table", "ChaCha20: Invalid key format ("..type(key).."), must be table")
113
        assert(type(nonce) == "table", "ChaCha20: Invalid nonce format ("..type(nonce).."), must be table")
114
        assert(#key == 16 or #key == 32, "ChaCha20: Invalid key length ("..#key.."), must be 16 or 32")
115
        assert(#nonce == 12, "ChaCha20: Invalid nonce length ("..#nonce.."), must be 12")
116

117
        local data = type(data) == "table" and {unpack(data)} or {tostring(data):byte(1,-1)}
118
        cntr = tonumber(cntr) or 1
119
        round = tonumber(round) or 20
120

121
        local out = {}
122
        local state = initState(key, nonce, cntr)
123
        local blockAmt = math.floor(#data/64)
124
        for i = 0, blockAmt do
125
            local ks = serialize(hashBlock(state, round))
126
            state[13] = (state[13]+1) % mod32
127

128
            local block = {}
129
            for j = 1, 64 do
130
                block[j] = data[((i)*64)+j]
131
            end
132
            for j = 1, #block do
133
                out[#out+1] = bxor(block[j], ks[j])
134
            end
135

136
            --[[if i % 1000 == 0 then
137
                os.queueEvent("")
138
                os.pullEvent("")
139
            end]]
140
        end
141
        return setmetatable(out, mt)
142
    end
143

144
    local function genNonce(len)
145
        local nonce = {}
146
        for i = 1, len do
147
            nonce[i] = math.random(0, 0xFF)
148
        end
149
        return setmetatable(nonce, mt)
150
    end
151

152
    local obj = {}
153
    local mtrng = {['__index'] = obj}
154
    local function newRNG(seed)
155
    local objVars = {}
156
    objVars.seed = seed
157
    objVars.cnt = 0
158
    objVars.block = {}
159
    return setmetatable(objVars, mtrng)
160
    end
161

162
    -- Specify how many bytes to return
163
    -- 1 Byte is 8 bits, returns int between 0 and 255
164
    -- 2 Bytes is 16 bits, returns int up to 65535
165
    -- 4 Bytes is 32 bits, returns int up to 4294967295
166
    -- Max of 6 Bytes is 48 bits, returns int up to 281474976710655
167
    function obj:nextInt(byte)
168
    if not byte or byte < 1 or byte > 6 then error("Can only return 1-6 bytes", 2) end
169
    local output = 0
170
    for i = 0, byte-1 do
171
        if #self.block == 0 then
172
        self.cnt = self.cnt + 1
173
        self.block = crypt(null32, self.seed, null12, self.cnt)
174
        end
175
        local newByte = table.remove(self.block)
176
        output = output + (newByte * (2^(8*i)))
177
    end
178
    return output
179
    end
180

181
    return {
182
        crypt = crypt,
183
        genNonce = genNonce,
184
        newRNG = newRNG
185
    }
186
end)()
187

188
-- START OF API --
189

190
settings.define("shironeko.pkey", {
191
    description = "Your Shironeko pkey, obtained from \\sn me",
192
    type = "string"
193
})
194

195
-- Wrap the modem with the given name with the given channel and privatekey
196
-- Creates a functionally secure modem peripheral with useful methods related to communication
197
local function wrap(name, channel, pkey)
198
    local key = table.pack(pkey:gsub("-", ""):byte(1,-1))
199
    local modem = assert(peripheral.wrap(name), "No such modem "..tostring(name))
200
    modem.open(channel)
201
    modem.name = name
202
    local transmit = modem.transmit
203
    modem.transmit = function(msg)
204
        local nonce = cha.genNonce(12)
205
        local data = cha.crypt(textutils.serialize(msg), key, nonce)
206
        transmit(channel,channel,{
207
            data = data,
208
            nonce = nonce
209
        })
210
    end
211
    modem.response = function(action, ok, result)
212
        modem.transmit({
213
            action = action,
214
            ok = ok,
215
            result = result
216
        })
217
    end
218

219
    modem.listen = function(callback, timeout)
220
        local ok = false
221
        local out
222
        parallel.waitForAny(function()
223
            local done = false
224
            while not done do
225
                local _, side, ch, _, msg = os.pullEvent("modem_message")
226
                if side == name and ch == channel then
227
                    local valid, response = pcall(function()
228
                        return textutils.unserialize(tostring(cha.crypt(msg.data, key, msg.nonce)))
229
                    end)
230
                    if valid then
231
                        if callback then
232
                            done, ok, out = callback(response)
233
                        else
234
                            done, ok, out = true, true, response
235
                        end
236
                    end
237
                end
238
            end
239
            return ok, out
240
        end,
241
        function()
242
            sleep(timeout or 5)
243
            out = "timeout"
244
        end)
245
        return ok, out
246
    end
247
    modem.await = function(callback)
248
        local done, ok = false, false
249
        local out
250
        while not done do
251
            local _, side, ch, _, msg = os.pullEvent("modem_message")
252
            if side == name and ch == channel then
253
                local valid, response = pcall(function()
254
                    return textutils.unserialize(tostring(cha.crypt(msg.data, key, msg.nonce)))
255
                end)
256
                if valid then
257
                    callback(response)
258
                end
259
            end
260
        end
261
    end
262
    return modem
263
end
264

265
local function responseCallback(action)
266
    return function(data)
267
        if data and data.action == action then
268
            return true, data.ok, data.result
269
        end
270
    end
271
end
272

273
-- Initialize a modem for use with Shironeko. Should be wireless.
274
-- Provides the run, send, and close functions.
275
local function init(modemName)
276
    expect(1, modemName, "string")
277
    local pkey = assert(settings.get("shironeko.pkey"), "Missing shironeko.pkey setting. See \\sn me to access it.")
278
    local modem = wrap(modemName, 4625, pkey)
279

280
    -- Emit events into the CC event loop when Shironeko sends them. Run this in parallel with your program.
281
    local function run()
282
        modem.await(function(data)
283
            if data.event then
284
                os.queueEvent("shironeko_"..data.event, data.data)
285
            end
286
        end)
287
    end
288

289
    -- Possible events emitted by run:
290
    -- shironeko_receive:
291
    -- data.items (table): table of items sent. May contain duplicate items that merged into the same stack on delivery.
292
    -- data.from (string): user that sent the items (the sender).
293
    -- data.fromLabel (string): label from which items were sent (sender's label).
294
    -- data.to (string): user these items were sent to (the receiver).
295
    -- data.toLabel (string): label to which items were sent (receiver's label).
296

297

298
    -- Send the provided slots to the provided user using the optional labels. fromLabel is one of the senders labels, toLabel is one of the recipients labels.
299
    -- Returns:
300
    -- boolean ok - Whether the operation was successful or not.
301
    -- if ok:
302
    -- table info - A table containing the same values as what would be provided by the recipient's shironeko_receive event, seen above.
303
    -- if not ok:
304
    -- string error - The nature of how the operation failed. See below function for possible errors.
305
    local function send(user, slots, fromLabel, toLabel)
306
        expect(1, user, "string")
307
        expect(2, slots, "table", "number")
308
        expect(3, fromLabel, "string", "nil")
309
        expect(4, toLabel, "string", "nil")
310
        modem.transmit({
311
            action = "send",
312
            user = user,
313
            slots = slots,
314
            fromLabel = fromLabel,
315
            toLabel = toLabel,
316
            version = version
317
        })
318
        return modem.listen(responseCallback("send"))
319
    end
320
    -- Possible errors for send:
321
    -- "timeout" - The operation timed out/Shironeko didn't respond.
322
    -- "version" - The API version used is out of date.
323
    -- "no_such_label_sender" - The sender (you) did not have the given fromLabel.
324
    -- "no_outbound_storage" - The sender's (you) label did not have a designated outbound storage.
325
    -- "no_such_user" - The recipient does not exist and/or does not have a Shironeko user.
326
    -- "no_such_label_recipient" - The recipient did not have the given toLabel.
327
    -- "no_inbound_storage" - The recipient's label did not have a designated inbound storage.
328
    -- "blocked" - You are blocked from performing this send operation by the recipient.
329
    -- "same_storage" - The fromLabel and toLabel point to the same storage.
330
    -- "recipient_full" - The recpient's inbound storage is full.
331

332

333
    -- Sends an amount of items specified by a list of tables containing item names, counts, and optional nbt values.
334
    -- In other words, the items in the list are identical to ones provided by generic inventory's .list() method.
335
    -- The only difference being that you can specify a count greater than 64 to easily send multiple stacks of an item.
336
    -- Returns:
337
    -- boolean ok - Whether the operation was successful or not.
338
    -- if ok:
339
    -- table info - A table containing the same values as what would be provided by the recipient's shironeko_receive event, seen above.
340
    -- if not ok:
341
    -- string error - The nature of how the operation failed. See below function for possible errors.
342
    local function sendItems(user, items, fromLabel, toLabel)
343
        expect(1, user, "string")
344
        expect(2, items, "table")
345
        expect(3, fromLabel, "string", "nil")
346
        expect(4, toLabel, "string", "nil")
347
        modem.transmit({
348
            action = "send_items",
349
            user = user,
350
            items = items,
351
            fromLabel = fromLabel,
352
            toLabel = toLabel,
353
            version = version
354
        })
355
        return modem.listen(responseCallback("send_items"))
356
    end
357
    -- Possible errors for sendItems:
358
    -- "timeout" - The operation timed out/Shironeko didn't respond.
359
    -- "version" - The API version used is out of date.
360
    -- "no_such_label_sender" - The sender (you) did not have the given fromLabel.
361
    -- "no_outbound_storage" - The sender's (you) label did not have a designated outbound storage.
362
    -- "no_such_user" - The recipient does not exist and/or does not have a Shironeko user.
363
    -- "no_such_label_recipient" - The recipient did not have the given toLabel.
364
    -- "no_inbound_storage" - The recipient's label did not have a designated inbound storage.
365
    -- "blocked" - You are blocked from performing this send operation by the recipient.
366
    -- "same_storage" - The fromLabel and toLabel point to the same storage.
367
    -- "recipient_full" - The recpient's inbound storage is full.
368

369

370
    -- Get a simple list of users who have a label "main" that can accept items.
371
    -- Returns:
372
    -- boolean ok - Whether the operation was successful or not.
373
    -- if ok:
374
    -- table users - List of users that have a "main" label that can accept items.
375
    -- if not ok:
376
    -- string error - The nature of how the operation failed. See below function for possible errors.
377
    local function getUsers()
378
        modem.transmit({
379
            action = "get_users",
380
            version = version
381
        })
382
        return modem.listen(responseCallback("get_users"))
383
    end
384
    -- Possible errors for send:
385
    -- "timeout" - The operation timed out/Shironeko didn't respond.
386
    -- "version" - The API version used is out of date.
387

388
    -- Close the modem channel used by Shironeko
389
    local function close()
390
        modem.close(4625)
391
    end
392

393
    return {
394
        run = run,
395
        send = send,
396
        sendItems = sendItems,
397
        getUsers = getUsers,
398
        close = close
399
    }
400
end
401

402
return {
403
    init = init,
404
}