implement hopper backend via beta config
This commit is contained in:
parent
d86ab173e1
commit
d367bf0f8d
10 changed files with 3198 additions and 537 deletions
|
|
@ -1,97 +0,0 @@
|
||||||
-- Copyright (c) 2015 Phil Leblanc -- see LICENSE file
|
|
||||||
------------------------------------------------------------
|
|
||||||
--[[
|
|
||||||
|
|
||||||
aead_chacha_poly
|
|
||||||
|
|
||||||
Authenticated Encryption with Associated Data (AEAD) [1], based
|
|
||||||
on Chacha20 stream encryption and Poly1305 MAC, as defined
|
|
||||||
in RFC 7539 [2].
|
|
||||||
|
|
||||||
[1] https://en.wikipedia.org/wiki/Authenticated_encryption
|
|
||||||
[2] http://www.rfc-editor.org/rfc/rfc7539.txt
|
|
||||||
|
|
||||||
This file uses chacha20.lua and poly1305 for the encryption
|
|
||||||
and MAC primitives.
|
|
||||||
|
|
||||||
]]
|
|
||||||
|
|
||||||
local chacha20 = require "lib.plc.chacha20"
|
|
||||||
local poly1305 = require "lib.plc.poly1305"
|
|
||||||
|
|
||||||
------------------------------------------------------------
|
|
||||||
-- poly1305 key generation
|
|
||||||
|
|
||||||
local poly_keygen = function(key, nonce)
|
|
||||||
local counter = 0
|
|
||||||
local m = string.rep('\0', 64)
|
|
||||||
local e = chacha20.encrypt(key, counter, nonce, m)
|
|
||||||
-- keep only first the 256 bits (32 bytes)
|
|
||||||
return e:sub(1, 32)
|
|
||||||
end
|
|
||||||
|
|
||||||
local pad16 = function(s)
|
|
||||||
-- return null bytes to add to s so that #s is a multiple of 16
|
|
||||||
return (#s % 16 == 0) and "" or ('\0'):rep(16 - (#s % 16))
|
|
||||||
end
|
|
||||||
|
|
||||||
local app = table.insert
|
|
||||||
|
|
||||||
local encrypt = function(aad, key, iv, constant, plain)
|
|
||||||
-- aad: additional authenticated data - arbitrary length
|
|
||||||
-- key: 32-byte string
|
|
||||||
-- iv, constant: concatenated to form the nonce (12 bytes)
|
|
||||||
-- (why not one 12-byte param? --maybe because IPsec uses
|
|
||||||
-- an 8-byte nonce)
|
|
||||||
-- implementation: RFC 7539 sect 2.8.1
|
|
||||||
-- (memory inefficient - encr text is copied in mac_data)
|
|
||||||
local mt = {} -- mac_data table
|
|
||||||
local nonce = constant .. iv
|
|
||||||
local otk = poly_keygen(key, nonce)
|
|
||||||
local encr = chacha20.encrypt(key, 1, nonce, plain)
|
|
||||||
app(mt, aad)
|
|
||||||
app(mt, pad16(aad))
|
|
||||||
app(mt, encr)
|
|
||||||
app(mt, pad16(encr))
|
|
||||||
-- aad and encrypted text length must be encoded as
|
|
||||||
-- little endian _u64_ (and not u32) -- see errata at
|
|
||||||
-- https://www.rfc-editor.org/errata_search.php?rfc=7539
|
|
||||||
app(mt, string.pack('<I8', #aad))
|
|
||||||
app(mt, string.pack('<I8', #encr))
|
|
||||||
local mac_data = table.concat(mt)
|
|
||||||
--~ p16('mac', mac_data)
|
|
||||||
local tag = poly1305.auth(mac_data, otk)
|
|
||||||
return encr, tag
|
|
||||||
end --chacha20_aead_encrypt()
|
|
||||||
|
|
||||||
local function decrypt(aad, key, iv, constant, encr, tag)
|
|
||||||
-- (memory inefficient - encr text is copied in mac_data)
|
|
||||||
-- (structure similar to aead_encrypt => what could be factored?)
|
|
||||||
local mt = {} -- mac_data table
|
|
||||||
local nonce = constant .. iv
|
|
||||||
local otk = poly_keygen(key, nonce)
|
|
||||||
app(mt, aad)
|
|
||||||
app(mt, pad16(aad))
|
|
||||||
app(mt, encr)
|
|
||||||
app(mt, pad16(encr))
|
|
||||||
app(mt, string.pack('<I8', #aad))
|
|
||||||
app(mt, string.pack('<I8', #encr))
|
|
||||||
local mac_data = table.concat(mt)
|
|
||||||
local mac = poly1305.auth(mac_data, otk)
|
|
||||||
if mac == tag then
|
|
||||||
local plain = chacha20.encrypt(key, 1, nonce, encr)
|
|
||||||
return plain
|
|
||||||
else
|
|
||||||
return nil, "auth failed"
|
|
||||||
end
|
|
||||||
end --chacha20_aead_decrypt()
|
|
||||||
|
|
||||||
|
|
||||||
------------------------------------------------------------
|
|
||||||
-- return aead_chacha_poly module
|
|
||||||
|
|
||||||
return {
|
|
||||||
poly_keygen = poly_keygen,
|
|
||||||
encrypt = encrypt,
|
|
||||||
decrypt = decrypt,
|
|
||||||
}
|
|
||||||
|
|
@ -1,225 +0,0 @@
|
||||||
local g = string.gsub
|
|
||||||
|
|
||||||
sha256 = loadstring(g(
|
|
||||||
g(
|
|
||||||
g(
|
|
||||||
g(
|
|
||||||
g(
|
|
||||||
g(
|
|
||||||
g(
|
|
||||||
g(
|
|
||||||
'Sa=XbandSb=XbxWSc=XlshiftSd=unpackSe=2^32SYf(g,h)Si=g/2^hSj=i%1Ui-j+j*eVSYk(l,m)Sn=l/2^mUn-n%1VSo={0x6a09e667Tbb67ae85T3c6ef372Ta54ff53aT510e527fT9b05688cT1f83d9abT5be0cd19}Sp={0x428a2f98T71374491Tb5c0fbcfTe9b5dba5T3956c25bT59f111f1T923f82a4Tab1c5ed5Td807aa98T12835b01T243185beT550c7dc3T72be5d74T80deb1feT9bdc06a7Tc19bf174Te49b69c1Tefbe4786T0fc19dc6T240ca1ccT2de92c6fT4a7484aaT5cb0a9dcT76f988daT983e5152Ta831c66dTb00327c8Tbf597fc7Tc6e00bf3Td5a79147T06ca6351T14292967T27b70a85T2e1b2138T4d2c6dfcT53380d13T650a7354T766a0abbT81c2c92eT92722c85Ta2bfe8a1Ta81a664bTc24b8b70Tc76c51a3Td192e819Td6990624Tf40e3585T106aa070T19a4c116T1e376c08T2748774cT34b0bcb5T391c0cb3T4ed8aa4aT5b9cca4fT682e6ff3T748f82eeT78a5636fT84c87814T8cc70208T90befffaTa4506cebTbef9a3f7Tc67178f2}SYq(r,q)if e-1-r[1]<q then r[2]=r[2]+1;r[1]=q-(e-1-r[1])-1 else r[1]=r[1]+qVUrVSYs(t)Su=#t;t[#t+1]=0x80;while#t%64~=56Zt[#t+1]=0VSv=q({0,0},u*8)fWw=2,1,-1Zt[#t+1]=a(k(a(v[w]TFF000000),24)TFF)t[#t+1]=a(k(a(v[w]TFF0000),16)TFF)t[#t+1]=a(k(a(v[w]TFF00),8)TFF)t[#t+1]=a(v[w]TFF)VUtVSYx(y,w)Uc(y[w]W0,24)+c(y[w+1]W0,16)+c(y[w+2]W0,8)+(y[w+3]W0)VSYz(t,w,A)SB={}fWC=1,16ZB[C]=x(t,w+(C-1)*4)VfWC=17,64ZSD=B[C-15]SE=b(b(f(B[C-15],7),f(B[C-15],18)),k(B[C-15],3))SF=b(b(f(B[C-2],17),f(B[C-2],19)),k(B[C-2],10))B[C]=(B[C-16]+E+B[C-7]+F)%eVSG,h,H,I,J,j,K,L=d(A)fWC=1,64ZSM=b(b(f(J,6),f(J,11)),f(J,25))SN=b(a(J,j),a(Xbnot(J),K))SO=(L+M+N+p[C]+B[C])%eSP=b(b(f(G,2),f(G,13)),f(G,22))SQ=b(b(a(G,h),a(G,H)),a(h,H))SR=(P+Q)%e;L,K,j,J,I,H,h,G=K,j,J,(I+O)%e,H,h,G,(O+R)%eVA[1]=(A[1]+G)%e;A[2]=(A[2]+h)%e;A[3]=(A[3]+H)%e;A[4]=(A[4]+I)%e;A[5]=(A[5]+J)%e;A[6]=(A[6]+j)%e;A[7]=(A[7]+K)%e;A[8]=(A[8]+L)%eUAVUY(t)t=t W""t=type(t)=="string"and{t:byte(1,-1)}Wt;t=s(t)SA={d(o)}fWw=1,#t,64ZA=z(t,w,A)VU("%08x"):rep(8):format(d(A))V',
|
|
||||||
"S", " local "), "T", ",0x"), "U", " return "), "V", " end "), "W", "or "), "X", "bit32."), "Y",
|
|
||||||
"function "), "Z",
|
|
||||||
" do "))()
|
|
||||||
|
|
||||||
local aead = require("lib.plc.aead_chacha_poly")
|
|
||||||
|
|
||||||
local function tobytes(s) return { s:byte(1, #s) } end
|
|
||||||
local function frombytes(t) return string.char(table.unpack(t)) end
|
|
||||||
|
|
||||||
local b64abc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
|
|
||||||
local function b64u_encode(bin)
|
|
||||||
local t, n, out = tobytes(bin), #bin, {}
|
|
||||||
local i = 1
|
|
||||||
while i <= n do
|
|
||||||
local a = t[i] or 0; local b = t[i + 1] or 0; local c = t[i + 2] or 0
|
|
||||||
local triple = a * 65536 + b * 256 + c
|
|
||||||
out[#out + 1] = b64abc:sub(bit32.band(bit32.rshift(triple, 18), 63) + 1, bit32.band(bit32.rshift(triple, 18), 63) + 1)
|
|
||||||
out[#out + 1] = b64abc:sub(bit32.band(bit32.rshift(triple, 12), 63) + 1, bit32.band(bit32.rshift(triple, 12), 63) + 1)
|
|
||||||
out[#out + 1] = i + 1 <= n and
|
|
||||||
b64abc:sub(bit32.band(bit32.rshift(triple, 6), 63) + 1, bit32.band(bit32.rshift(triple, 6), 63) + 1) or ''
|
|
||||||
out[#out + 1] = i + 2 <= n and
|
|
||||||
b64abc:sub(bit32.band(bit32.rshift(triple, 0), 63) + 1, bit32.band(bit32.rshift(triple, 0), 63) + 1) or ''
|
|
||||||
i = i + 3
|
|
||||||
end
|
|
||||||
return table.concat(out)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function b64u_decode(txt)
|
|
||||||
local map = {}
|
|
||||||
for i = 1, #b64abc do map[b64abc:sub(i, i)] = i - 1 end
|
|
||||||
local out = {}
|
|
||||||
local i = 1
|
|
||||||
while i <= #txt do
|
|
||||||
local a = map[txt:sub(i, i)]; i = i + 1
|
|
||||||
local b = map[txt:sub(i, i)]; i = i + 1
|
|
||||||
local c = map[txt:sub(i, i)]; i = i + 1
|
|
||||||
local d = map[txt:sub(i, i)]; i = i + 1
|
|
||||||
if a == nil or b == nil then break end
|
|
||||||
local triple = bit32.bor(bit32.lshift(a, 18), bit32.lshift(b, 12), bit32.lshift(c or 0, 6), (d or 0))
|
|
||||||
out[#out + 1] = string.char(bit32.band(bit32.rshift(triple, 16), 255))
|
|
||||||
if c ~= nil then out[#out + 1] = string.char(bit32.band(bit32.rshift(triple, 8), 255)) end
|
|
||||||
if d ~= nil then out[#out + 1] = string.char(bit32.band(triple, 255)) end
|
|
||||||
end
|
|
||||||
return table.concat(out)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function hmac_sha256(key, msg)
|
|
||||||
local block = 64
|
|
||||||
if #key > block then key = (sha256(key):gsub('..', function(h) return string.char(tonumber(h, 16)) end)) end
|
|
||||||
if #key < block then key = key .. string.rep("\0", block - #key) end
|
|
||||||
local o_key_pad = key:gsub('.', function(c) return string.char(bit32.bxor(string.byte(c), 0x5c)) end)
|
|
||||||
local i_key_pad = key:gsub('.', function(c) return string.char(bit32.bxor(string.byte(c), 0x36)) end)
|
|
||||||
local inner = sha256(i_key_pad .. msg)
|
|
||||||
inner = inner:gsub('..', function(h) return string.char(tonumber(h, 16)) end)
|
|
||||||
local mac = sha256(o_key_pad .. inner)
|
|
||||||
return mac:gsub('..', function(h) return string.char(tonumber(h, 16)) end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function int_be(n) -- 32-bit BE
|
|
||||||
return string.char(bit32.band(bit32.rshift(n, 24), 255), bit32.band(bit32.rshift(n, 16), 255),
|
|
||||||
bit32.band(bit32.rshift(n, 8), 255), bit32.band(n, 255))
|
|
||||||
end
|
|
||||||
|
|
||||||
local function pbkdf2_sha256(password, salt, iterations, dkLen)
|
|
||||||
local hLen = 32
|
|
||||||
local l = math.ceil(dkLen / hLen)
|
|
||||||
local r = dkLen - (l - 1) * hLen
|
|
||||||
local dk = {}
|
|
||||||
for i = 1, l do
|
|
||||||
local U = hmac_sha256(password, salt .. int_be(i))
|
|
||||||
local T = { U:byte(1, hLen) }
|
|
||||||
for itr = 2, iterations do
|
|
||||||
U = hmac_sha256(password, U)
|
|
||||||
local Ub = { U:byte(1, hLen) }
|
|
||||||
for j = 1, hLen do T[j] = bit32.bxor(T[j], Ub[j]) end
|
|
||||||
if itr % 500 == 0 then sleep(0) end
|
|
||||||
end
|
|
||||||
if i < l then
|
|
||||||
dk[#dk + 1] = string.char(table.unpack(T))
|
|
||||||
else
|
|
||||||
dk[#dk + 1] = string.char(table.unpack(T, 1, r))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return table.concat(dk)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- HMAC-DRBG
|
|
||||||
local SEED_PATH = "siss_drg_seed.bin"
|
|
||||||
local DRBG = { K = string.rep("\0", 32), V = string.rep("\1", 32), inited = false }
|
|
||||||
|
|
||||||
local function drbg_update(provided_data)
|
|
||||||
DRBG.K = hmac_sha256(DRBG.K, DRBG.V .. "\0" .. (provided_data or ""))
|
|
||||||
DRBG.V = hmac_sha256(DRBG.K, DRBG.V)
|
|
||||||
if provided_data and #provided_data > 0 then
|
|
||||||
DRBG.K = hmac_sha256(DRBG.K, DRBG.V .. "\1" .. provided_data)
|
|
||||||
DRBG.V = hmac_sha256(DRBG.K, DRBG.V)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function drbg_init(seed)
|
|
||||||
DRBG.K = string.rep("\0", 32)
|
|
||||||
DRBG.V = string.rep("\1", 32)
|
|
||||||
drbg_update(seed)
|
|
||||||
DRBG.inited = true
|
|
||||||
end
|
|
||||||
|
|
||||||
local function drbg_bytes(n)
|
|
||||||
if not DRBG.inited then error("DRBG not seeded. Call seed_entropy() once.") end
|
|
||||||
local out = {}
|
|
||||||
while #table.concat(out) < n do
|
|
||||||
DRBG.V = hmac_sha256(DRBG.K, DRBG.V)
|
|
||||||
out[#out + 1] = DRBG.V
|
|
||||||
end
|
|
||||||
drbg_update("")
|
|
||||||
local buf = table.concat(out)
|
|
||||||
return buf:sub(1, n)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function seed_entropy(extra_bytes)
|
|
||||||
local accum = {}
|
|
||||||
if fs.exists(SEED_PATH) then
|
|
||||||
local f = fs.open(SEED_PATH, "rb"); accum[#accum + 1] = f.readAll(); f.close()
|
|
||||||
local seed = sha256(table.concat(accum)):gsub('..', function(h) return string.char(tonumber(h, 16)) end)
|
|
||||||
drbg_init(seed)
|
|
||||||
else
|
|
||||||
term.write("Move around / mash keys then press Enter 4 times to seedâ¦\n")
|
|
||||||
for i = 1, 4 do
|
|
||||||
local t1 = os.clock(); read(); local t2 = os.clock()
|
|
||||||
accum[#accum + 1] = int_be(math.floor((t2 - t1) * 1e9)) .. tostring({}):sub(8)
|
|
||||||
end
|
|
||||||
if extra_bytes and #extra_bytes > 0 then accum[#accum + 1] = extra_bytes end
|
|
||||||
local seed = sha256(table.concat(accum)):gsub('..', function(h) return string.char(tonumber(h, 16)) end)
|
|
||||||
drbg_init(seed)
|
|
||||||
local f = fs.open(SEED_PATH, "wb")
|
|
||||||
f.write(drbg_bytes(48))
|
|
||||||
f.close()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local VERSION = "\1"
|
|
||||||
local KDF_ID = "\1"
|
|
||||||
local AAD = "SiSS RA rev1|chacha20poly1305"
|
|
||||||
|
|
||||||
local function u32be(n)
|
|
||||||
return string.char(bit32.band(bit32.rshift(n, 24), 255), bit32.band(bit32.rshift(n, 16), 255),
|
|
||||||
bit32.band(bit32.rshift(n, 8), 255), bit32.band(n, 255))
|
|
||||||
end
|
|
||||||
local function u16be(n) return string.char(bit32.band(bit32.rshift(n, 8), 255), bit32.band(n, 255)) end
|
|
||||||
local function pack_params(iter)
|
|
||||||
return u32be(iter) .. u16be(32)
|
|
||||||
end
|
|
||||||
local function unpack_params(s)
|
|
||||||
local i1, i2, i3, i4, d1, d2 = s:byte(1, 6)
|
|
||||||
local iters = (bit32.bor(bit32.lshift(i1, 24), bit32.lshift(i2, 16), bit32.lshift(i3, 8), i4))
|
|
||||||
local dklen = (bit32.bor(bit32.lshift(d1, 8), d2))
|
|
||||||
return iters, dklen
|
|
||||||
end
|
|
||||||
|
|
||||||
local function random_bytes(n) return drbg_bytes(n) end
|
|
||||||
|
|
||||||
local function split_nonce12(nonce12)
|
|
||||||
assert(#nonce12 == 12, "nonce must be 12 bytes")
|
|
||||||
local iv = nonce12:sub(1, 6)
|
|
||||||
local constant = nonce12:sub(7, 12)
|
|
||||||
return iv, constant
|
|
||||||
end
|
|
||||||
|
|
||||||
local function encrypt_with_password(password, plaintext)
|
|
||||||
local salt = random_bytes(16)
|
|
||||||
local iters = 1000
|
|
||||||
local key = pbkdf2_sha256(password, salt, iters, 32)
|
|
||||||
|
|
||||||
local nonce12 = random_bytes(12)
|
|
||||||
local iv, constant = split_nonce12(nonce12)
|
|
||||||
|
|
||||||
local ciphertext, tag = aead.encrypt(AAD, key, iv, constant, plaintext)
|
|
||||||
|
|
||||||
local params = pack_params(iters)
|
|
||||||
local blob = table.concat { VERSION, KDF_ID, params, salt, nonce12, ciphertext, tag }
|
|
||||||
return b64u_encode(blob)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function decrypt_with_password(password, blob)
|
|
||||||
local bin = b64u_decode(blob)
|
|
||||||
if #bin < 1 + 1 + 6 + 16 + 12 + 16 then return nil, "ciphertext too short" end
|
|
||||||
local pos = 1
|
|
||||||
local ver = bin:sub(pos, pos); pos = pos + 1
|
|
||||||
if ver ~= VERSION then return nil, "version mismatch" end
|
|
||||||
local kdfid = bin:sub(pos, pos); pos = pos + 1
|
|
||||||
if kdfid ~= KDF_ID then return nil, "kdf mismatch" end
|
|
||||||
local params = bin:sub(pos, pos + 5); pos = pos + 6
|
|
||||||
local iters, dklen = unpack_params(params); if dklen ~= 32 then return nil, "bad dkLen" end
|
|
||||||
local salt = bin:sub(pos, pos + 15); pos = pos + 16
|
|
||||||
|
|
||||||
-- read the same 12-byte nonce and split the same way
|
|
||||||
local nonce12 = bin:sub(pos, pos + 11); pos = pos + 12
|
|
||||||
local iv, constant = split_nonce12(nonce12)
|
|
||||||
|
|
||||||
local tag_len = 16
|
|
||||||
local tag = bin:sub(#bin - tag_len + 1)
|
|
||||||
local ciphertext = bin:sub(pos, #bin - tag_len)
|
|
||||||
|
|
||||||
local key = pbkdf2_sha256(password, salt, iters, 32)
|
|
||||||
|
|
||||||
local pt, err = aead.decrypt(AAD, key, iv, constant, ciphertext, tag)
|
|
||||||
if not pt then return nil, err or "auth failed" end
|
|
||||||
return pt
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
decrypt_with_password = decrypt_with_password,
|
|
||||||
encrypt_with_password = encrypt_with_password,
|
|
||||||
seed_entropy = seed_entropy
|
|
||||||
}
|
|
||||||
|
|
@ -1,204 +0,0 @@
|
||||||
-- Copyright (c) 2015 Phil Leblanc -- see LICENSE file
|
|
||||||
------------------------------------------------------------
|
|
||||||
--[[
|
|
||||||
|
|
||||||
Poly1305 message authentication (MAC) created by Dan Bernstein
|
|
||||||
...
|
|
||||||
]]
|
|
||||||
|
|
||||||
-----------------------------------------------------------
|
|
||||||
-- poly1305
|
|
||||||
|
|
||||||
local sunp = string.unpack
|
|
||||||
local bit32 = bit32 -- alias
|
|
||||||
|
|
||||||
local function poly_init(k)
|
|
||||||
-- k: 32-byte key as a string
|
|
||||||
-- initialize internal state
|
|
||||||
local st = {
|
|
||||||
r = {
|
|
||||||
bit32.band(sunp('<I4', k, 1), 0x3ffffff), -- r0
|
|
||||||
bit32.band(bit32.rshift(sunp('<I4', k, 4), 2), 0x3ffff03), -- r1
|
|
||||||
bit32.band(bit32.rshift(sunp('<I4', k, 7), 4), 0x3ffc0ff), -- r2
|
|
||||||
bit32.band(bit32.rshift(sunp('<I4', k, 10), 6), 0x3f03fff), -- r3
|
|
||||||
bit32.band(bit32.rshift(sunp('<I4', k, 13), 8), 0x00fffff), -- r4
|
|
||||||
},
|
|
||||||
h = { 0, 0, 0, 0, 0 },
|
|
||||||
pad = { sunp('<I4', k, 17), -- 's' in rfc
|
|
||||||
sunp('<I4', k, 21),
|
|
||||||
sunp('<I4', k, 25),
|
|
||||||
sunp('<I4', k, 29),
|
|
||||||
},
|
|
||||||
buffer = "", --
|
|
||||||
leftover = 0,
|
|
||||||
final = false,
|
|
||||||
} --st
|
|
||||||
return st
|
|
||||||
end --poly_init()
|
|
||||||
|
|
||||||
local function poly_blocks(st, m)
|
|
||||||
-- st: internal state
|
|
||||||
-- m: message:string
|
|
||||||
local bytes = #m
|
|
||||||
local midx = 1
|
|
||||||
local hibit = st.final and 0 or 0x01000000 -- 1 << 24
|
|
||||||
local r0 = st.r[1]
|
|
||||||
local r1 = st.r[2]
|
|
||||||
local r2 = st.r[3]
|
|
||||||
local r3 = st.r[4]
|
|
||||||
local r4 = st.r[5]
|
|
||||||
local s1 = r1 * 5
|
|
||||||
local s2 = r2 * 5
|
|
||||||
local s3 = r3 * 5
|
|
||||||
local s4 = r4 * 5
|
|
||||||
local h0 = st.h[1]
|
|
||||||
local h1 = st.h[2]
|
|
||||||
local h2 = st.h[3]
|
|
||||||
local h3 = st.h[4]
|
|
||||||
local h4 = st.h[5]
|
|
||||||
local d0, d1, d2, d3, d4, c
|
|
||||||
--
|
|
||||||
while bytes >= 16 do -- 16 = poly1305_block_size
|
|
||||||
-- h += m[i] (in rfc: a += n with 0x01 byte)
|
|
||||||
h0 = h0 + bit32.band(sunp('<I4', m, midx), 0x3ffffff)
|
|
||||||
h1 = h1 + bit32.band(bit32.rshift(sunp('<I4', m, midx + 3), 2), 0x3ffffff)
|
|
||||||
h2 = h2 + bit32.band(bit32.rshift(sunp('<I4', m, midx + 6), 4), 0x3ffffff)
|
|
||||||
h3 = h3 + bit32.band(bit32.rshift(sunp('<I4', m, midx + 9), 6), 0x3ffffff)
|
|
||||||
h4 = h4 + bit32.bor(bit32.rshift(sunp('<I4', m, midx + 12), 8), hibit) -- 0x01 byte
|
|
||||||
--
|
|
||||||
-- h *= r % p (partial)
|
|
||||||
d0 = h0 * r0 + h1 * s4 + h2 * s3 + h3 * s2 + h4 * s1
|
|
||||||
d1 = h0 * r1 + h1 * r0 + h2 * s4 + h3 * s3 + h4 * s2
|
|
||||||
d2 = h0 * r2 + h1 * r1 + h2 * r0 + h3 * s4 + h4 * s3
|
|
||||||
d3 = h0 * r3 + h1 * r2 + h2 * r1 + h3 * r0 + h4 * s4
|
|
||||||
d4 = h0 * r4 + h1 * r3 + h2 * r2 + h3 * r1 + h4 * r0
|
|
||||||
--
|
|
||||||
c = bit32.band(bit32.rshift(d0, 26), 0xffffffff); h0 = bit32.band(d0, 0x3ffffff)
|
|
||||||
d1 = d1 + c; c = bit32.band(bit32.rshift(d1, 26), 0xffffffff); h1 = bit32.band(d1, 0x3ffffff)
|
|
||||||
d2 = d2 + c; c = bit32.band(bit32.rshift(d2, 26), 0xffffffff); h2 = bit32.band(d2, 0x3ffffff)
|
|
||||||
d3 = d3 + c; c = bit32.band(bit32.rshift(d3, 26), 0xffffffff); h3 = bit32.band(d3, 0x3ffffff)
|
|
||||||
d4 = d4 + c; c = bit32.band(bit32.rshift(d4, 26), 0xffffffff); h4 = bit32.band(d4, 0x3ffffff)
|
|
||||||
h0 = h0 + (c * 5); c = bit32.rshift(h0, 26); h0 = bit32.band(h0, 0x3ffffff)
|
|
||||||
h1 = h1 + c
|
|
||||||
--
|
|
||||||
midx = midx + 16 -- 16 = poly1305_block_size
|
|
||||||
bytes = bytes - 16
|
|
||||||
end --while
|
|
||||||
st.h[1] = h0
|
|
||||||
st.h[2] = h1
|
|
||||||
st.h[3] = h2
|
|
||||||
st.h[4] = h3
|
|
||||||
st.h[5] = h4
|
|
||||||
st.bytes = bytes -- remaining bytes. must be < 16 here
|
|
||||||
st.midx = midx -- index of first remaining bytes
|
|
||||||
return st
|
|
||||||
end --poly_blocks()
|
|
||||||
|
|
||||||
local function poly_update(st, m)
|
|
||||||
-- st: internal state
|
|
||||||
-- m: message:string
|
|
||||||
st.bytes, st.midx = #m, 1
|
|
||||||
-- process full blocks if any
|
|
||||||
if st.bytes >= 16 then
|
|
||||||
poly_blocks(st, m)
|
|
||||||
end
|
|
||||||
--handle remaining bytes
|
|
||||||
if st.bytes == 0 then -- no bytes left
|
|
||||||
-- nothing to do? no add 0x01? - apparently not.
|
|
||||||
else
|
|
||||||
local buffer = string.sub(m, st.midx)
|
|
||||||
.. '\x01' .. string.rep('\0', 16 - st.bytes - 1)
|
|
||||||
assert(#buffer == 16)
|
|
||||||
st.final = true -- this is the last block
|
|
||||||
--~ p16(buffer)
|
|
||||||
poly_blocks(st, buffer)
|
|
||||||
end
|
|
||||||
--
|
|
||||||
return st
|
|
||||||
end --poly_update
|
|
||||||
|
|
||||||
local function poly_finish(st)
|
|
||||||
--
|
|
||||||
local c, mask --u32
|
|
||||||
local f --u64
|
|
||||||
-- fully carry h
|
|
||||||
local h0 = st.h[1]
|
|
||||||
local h1 = st.h[2]
|
|
||||||
local h2 = st.h[3]
|
|
||||||
local h3 = st.h[4]
|
|
||||||
local h4 = st.h[5]
|
|
||||||
--
|
|
||||||
c = bit32.rshift(h1, 26); h1 = bit32.band(h1, 0x3ffffff)
|
|
||||||
h2 = h2 + c; c = bit32.rshift(h2, 26); h2 = bit32.band(h2, 0x3ffffff)
|
|
||||||
h3 = h3 + c; c = bit32.rshift(h3, 26); h3 = bit32.band(h3, 0x3ffffff)
|
|
||||||
h4 = h4 + c; c = bit32.rshift(h4, 26); h4 = bit32.band(h4, 0x3ffffff)
|
|
||||||
h0 = h0 + (c * 5); c = bit32.rshift(h0, 26); h0 = bit32.band(h0, 0x3ffffff)
|
|
||||||
h1 = h1 + c
|
|
||||||
--
|
|
||||||
--compute h + -p
|
|
||||||
local g0 = (h0 + 5); c = bit32.rshift(g0, 26); g0 = bit32.band(g0, 0x3ffffff)
|
|
||||||
local g1 = (h1 + c); c = bit32.rshift(g1, 26); g1 = bit32.band(g1, 0x3ffffff)
|
|
||||||
local g2 = (h2 + c); c = bit32.rshift(g2, 26); g2 = bit32.band(g2, 0x3ffffff)
|
|
||||||
local g3 = (h3 + c); c = bit32.rshift(g3, 26); g3 = bit32.band(g3, 0x3ffffff)
|
|
||||||
local g4 = bit32.band(h4 + c - 0x4000000, 0xffffffff) -- (1 << 26)
|
|
||||||
--
|
|
||||||
-- select h if h < p, or h + -p if h >= p
|
|
||||||
mask = bit32.band(bit32.rshift(g4, 31) - 1, 0xffffffff)
|
|
||||||
--
|
|
||||||
g0 = bit32.band(g0, mask)
|
|
||||||
g1 = bit32.band(g1, mask)
|
|
||||||
g2 = bit32.band(g2, mask)
|
|
||||||
g3 = bit32.band(g3, mask)
|
|
||||||
g4 = bit32.band(g4, mask)
|
|
||||||
--
|
|
||||||
mask = bit32.band(bit32.bnot(mask), 0xffffffff)
|
|
||||||
h0 = bit32.bor(bit32.band(h0, mask), g0)
|
|
||||||
h1 = bit32.bor(bit32.band(h1, mask), g1)
|
|
||||||
h2 = bit32.bor(bit32.band(h2, mask), g2)
|
|
||||||
h3 = bit32.bor(bit32.band(h3, mask), g3)
|
|
||||||
h4 = bit32.bor(bit32.band(h4, mask), g4)
|
|
||||||
--
|
|
||||||
--h = h % (2^128)
|
|
||||||
h0 = bit32.band(bit32.bor((h0), bit32.lshift(h1, 26)), 0xffffffff)
|
|
||||||
h1 = bit32.band(bit32.bor(bit32.rshift(h1, 6), bit32.lshift(h2, 20)), 0xffffffff)
|
|
||||||
h2 = bit32.band(bit32.bor(bit32.rshift(h2, 12), bit32.lshift(h3, 14)), 0xffffffff)
|
|
||||||
h3 = bit32.band(bit32.bor(bit32.rshift(h3, 18), bit32.lshift(h4, 8)), 0xffffffff)
|
|
||||||
--
|
|
||||||
-- mac = (h + pad) % (2^128)
|
|
||||||
f = h0 + st.pad[1]; h0 = bit32.band(f, 0xffffffff)
|
|
||||||
f = h1 + st.pad[2] + bit32.rshift(f, 32); h1 = bit32.band(f, 0xffffffff)
|
|
||||||
f = h2 + st.pad[3] + bit32.rshift(f, 32); h2 = bit32.band(f, 0xffffffff)
|
|
||||||
f = h3 + st.pad[4] + bit32.rshift(f, 32); h3 = bit32.band(f, 0xffffffff)
|
|
||||||
--
|
|
||||||
local mac = string.pack('<I4I4I4I4', h0, h1, h2, h3)
|
|
||||||
-- (should zero out the state?)
|
|
||||||
--
|
|
||||||
return mac
|
|
||||||
end --poly_finish()
|
|
||||||
|
|
||||||
local function poly_auth(m, k)
|
|
||||||
-- m: msg string
|
|
||||||
-- k: key string (must be 32 bytes)
|
|
||||||
-- return mac 16-byte string
|
|
||||||
assert(#k == 32)
|
|
||||||
local st = poly_init(k)
|
|
||||||
poly_update(st, m)
|
|
||||||
local mac = poly_finish(st)
|
|
||||||
return mac
|
|
||||||
end --poly_auth()
|
|
||||||
|
|
||||||
local function poly_verify(m, k, mac)
|
|
||||||
local macm = poly_auth(m, k)
|
|
||||||
return macm == mac
|
|
||||||
end --poly_verify()
|
|
||||||
|
|
||||||
------------------------------------------------------------
|
|
||||||
-- return poly1305 module
|
|
||||||
|
|
||||||
return {
|
|
||||||
init = poly_init,
|
|
||||||
update = poly_update,
|
|
||||||
finish = poly_finish,
|
|
||||||
auth = poly_auth,
|
|
||||||
verify = poly_verify,
|
|
||||||
}
|
|
||||||
2983
src/lib/hopper.lua
Normal file
2983
src/lib/hopper.lua
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -409,12 +409,12 @@ function PrimeUI.selectionBox(win, x, y, width, height, entries, action, selectC
|
||||||
end
|
end
|
||||||
-- Redraw screen.
|
-- Redraw screen.
|
||||||
drawEntries()
|
drawEntries()
|
||||||
elseif key == keys.enter then
|
elseif key ~= keys.up and key ~= keys.down then
|
||||||
-- Select the entry: send the action.
|
-- Select the entry: send the action.
|
||||||
if type(action) == "string" then
|
if type(action) == "string" then
|
||||||
PrimeUI.resolve("selectionBox", action, entries()[selection])
|
PrimeUI.resolve("selectionBox", action, entries()[selection], key)
|
||||||
else
|
else
|
||||||
action(entries()[selection])
|
action(entries()[selection], key)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif event == "mouse_click" and key == 1 then
|
elseif event == "mouse_click" and key == 1 then
|
||||||
|
|
@ -503,6 +503,48 @@ function PrimeUI.selectionBox(win, x, y, width, height, entries, action, selectC
|
||||||
return drawEntries
|
return drawEntries
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Draws a thin border around a screen region.
|
||||||
|
---@param win window The window to draw on
|
||||||
|
---@param x number The X coordinate of the inside of the box
|
||||||
|
---@param y number The Y coordinate of the inside of the box
|
||||||
|
---@param width number The width of the inner box
|
||||||
|
---@param height number The height of the inner box
|
||||||
|
---@param fgColor color|nil The color of the border (defaults to white)
|
||||||
|
---@param bgColor color|nil The color of the background (defaults to black)
|
||||||
|
function PrimeUI.borderBox(win, x, y, width, height, fgColor, bgColor)
|
||||||
|
expect(1, win, "table")
|
||||||
|
expect(2, x, "number")
|
||||||
|
expect(3, y, "number")
|
||||||
|
expect(4, width, "number")
|
||||||
|
expect(5, height, "number")
|
||||||
|
fgColor = expect(6, fgColor, "number", "nil") or colors.white
|
||||||
|
bgColor = expect(7, bgColor, "number", "nil") or colors.black
|
||||||
|
-- Draw the top-left corner & top border.
|
||||||
|
win.setBackgroundColor(bgColor)
|
||||||
|
win.setTextColor(fgColor)
|
||||||
|
win.setCursorPos(x - 1, y - 1)
|
||||||
|
win.write("\x9C" .. ("\x8C"):rep(width))
|
||||||
|
-- Draw the top-right corner.
|
||||||
|
win.setBackgroundColor(fgColor)
|
||||||
|
win.setTextColor(bgColor)
|
||||||
|
win.write("\x93")
|
||||||
|
-- Draw the right border.
|
||||||
|
for i = 1, height do
|
||||||
|
win.setCursorPos(win.getCursorPos() - 1, y + i - 1)
|
||||||
|
win.write("\x95")
|
||||||
|
end
|
||||||
|
-- Draw the left border.
|
||||||
|
win.setBackgroundColor(bgColor)
|
||||||
|
win.setTextColor(fgColor)
|
||||||
|
for i = 1, height do
|
||||||
|
win.setCursorPos(x - 1, y + i - 1)
|
||||||
|
win.write("\x95")
|
||||||
|
end
|
||||||
|
-- Draw the bottom border and corners.
|
||||||
|
win.setCursorPos(x - 1, y + height)
|
||||||
|
win.write("\x8D" .. ("\x8C"):rep(width) .. "\x8E")
|
||||||
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
PrimeUI = PrimeUI
|
PrimeUI = PrimeUI
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ function run()
|
||||||
amount = tonumber(args[3], 10)
|
amount = tonumber(args[3], 10)
|
||||||
end
|
end
|
||||||
|
|
||||||
local moved = inv.sendItemAwayMultiple(slots, peripInventory, peripInventory, amount)
|
local moved = inv.sendItemAwayMultiple(slots, peripInventory, config.chatbox.players[user], amount)
|
||||||
|
|
||||||
chatbox.tell(user, "Moved `" .. tostring(moved) .. "` items.", BOT_NAME)
|
chatbox.tell(user, "Moved `" .. tostring(moved) .. "` items.", BOT_NAME)
|
||||||
elseif args[1] == "withdraw" then
|
elseif args[1] == "withdraw" then
|
||||||
|
|
@ -196,7 +196,7 @@ function run()
|
||||||
amount = tonumber(args[3], 10)
|
amount = tonumber(args[3], 10)
|
||||||
end
|
end
|
||||||
|
|
||||||
local moved = inv.sendItemToSelf(itemName, peripInventory, amount)
|
local moved = inv.sendItemToSelf(itemName, peripInventory, amount, config.chatbox.players[user])
|
||||||
|
|
||||||
chatbox.tell(user, "Moved `" .. tostring(moved) .. "` items.", BOT_NAME)
|
chatbox.tell(user, "Moved `" .. tostring(moved) .. "` items.", BOT_NAME)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
local config = require("../../config") ---@type Config
|
local config = require("../../config") ---@type Config
|
||||||
|
|
||||||
if config.inventories == nil and config.remote.connection then
|
if config.inventories == nil and config.remote.connection then
|
||||||
return require("modules.inventory_layers.inv_ra")
|
return require("modules.inventory_layers.inv_ra")
|
||||||
else
|
else
|
||||||
|
if config.beta and config.beta.hopper ~= nil then
|
||||||
|
return require("modules.inventory_layers.inv_hopper")
|
||||||
|
end
|
||||||
return require("modules.inventory_layers.inv_ail")
|
return require("modules.inventory_layers.inv_ail")
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@ end
|
||||||
---@param itemName string
|
---@param itemName string
|
||||||
---@param perip ccTweaked.peripheral.Inventory|string|nil
|
---@param perip ccTweaked.peripheral.Inventory|string|nil
|
||||||
---@param maxAmount number|nil
|
---@param maxAmount number|nil
|
||||||
local function sendItemToSelf(itemName, perip, maxAmount)
|
---@param id string|nil
|
||||||
|
local function sendItemToSelf(itemName, perip, maxAmount, id)
|
||||||
if perip == nil then
|
if perip == nil then
|
||||||
perip = turtleId
|
perip = turtleId
|
||||||
end
|
end
|
||||||
|
|
|
||||||
142
src/modules/inventory_layers/inv_hopper.lua
Normal file
142
src/modules/inventory_layers/inv_hopper.lua
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
local previousInventory = {}
|
||||||
|
local turtleMoveAllowed = true
|
||||||
|
local config = require("../../config") ---@type Config
|
||||||
|
|
||||||
|
local hopper = require("lib.hopper")
|
||||||
|
|
||||||
|
local hopperInventories = table.concat(config.inventories, "|");
|
||||||
|
|
||||||
|
local function sync()
|
||||||
|
hopper("-storage store " .. hopperInventories)
|
||||||
|
print("Synced.");
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param itemName string
|
||||||
|
---@param perip ccTweaked.peripheral.Inventory|string|nil
|
||||||
|
---@param maxAmount number|nil
|
||||||
|
---@param id string|nil
|
||||||
|
local function sendItemToSelf(itemName, perip, maxAmount, id)
|
||||||
|
if perip == nil then
|
||||||
|
perip = "self"
|
||||||
|
end
|
||||||
|
local rid = ""
|
||||||
|
if id == nil then
|
||||||
|
if type(perip) ~= "string" then
|
||||||
|
return
|
||||||
|
else
|
||||||
|
rid = perip
|
||||||
|
end
|
||||||
|
else
|
||||||
|
rid = id;
|
||||||
|
end
|
||||||
|
turtleMoveAllowed = false
|
||||||
|
|
||||||
|
local moved = hopper("store " .. rid .. " " .. itemName .. " -transfer-limit " .. tostring(maxAmount or 64))
|
||||||
|
turtleMoveAllowed = true
|
||||||
|
|
||||||
|
return moved
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param slots ccTweaked.turtle.turtleSlot[]
|
||||||
|
---@param perip ccTweaked.peripheral.Inventory|nil
|
||||||
|
---@param id string|nil
|
||||||
|
---@param maxAmount number|nil
|
||||||
|
local function sendItemAwayMultiple(slots, perip, id, maxAmount)
|
||||||
|
if perip == nil then
|
||||||
|
perip = turtle
|
||||||
|
end
|
||||||
|
local srcId = id or "self"
|
||||||
|
|
||||||
|
if srcId == nil then return end
|
||||||
|
|
||||||
|
local hopslots = ""
|
||||||
|
|
||||||
|
for _, slot in pairs(slots) do
|
||||||
|
hopslots = hopslots .. " -from-slot " .. tostring(slot)
|
||||||
|
end
|
||||||
|
|
||||||
|
return hopper(srcId .. " store -from-limit-max " .. (maxAmount or 64) .. hopslots .. " -per-slot")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getTurtleInventory()
|
||||||
|
local inventory = {}
|
||||||
|
for slot = 1, 16 do
|
||||||
|
local item = turtle.getItemDetail(slot, false)
|
||||||
|
inventory[slot] = item
|
||||||
|
end
|
||||||
|
return inventory
|
||||||
|
end
|
||||||
|
|
||||||
|
local function detectPlayerInsert()
|
||||||
|
if turtle then
|
||||||
|
previousInventory = getTurtleInventory()
|
||||||
|
|
||||||
|
while true do
|
||||||
|
os.pullEvent("turtle_inventory")
|
||||||
|
|
||||||
|
local currentInventory = getTurtleInventory()
|
||||||
|
|
||||||
|
if turtleMoveAllowed then
|
||||||
|
local newlyAddedSlots = {}
|
||||||
|
|
||||||
|
for slot = 1, 16 do
|
||||||
|
local prev = previousInventory[slot]
|
||||||
|
local curr = currentInventory[slot]
|
||||||
|
|
||||||
|
if not prev and curr then
|
||||||
|
table.insert(newlyAddedSlots, slot)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #newlyAddedSlots > 0 then
|
||||||
|
sendItemAwayMultiple(newlyAddedSlots)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
previousInventory = currentInventory
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function run()
|
||||||
|
return function()
|
||||||
|
-- this needs to do something
|
||||||
|
print("inv.run ran")
|
||||||
|
while true do
|
||||||
|
sleep(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function listItemAmounts()
|
||||||
|
return hopper.list("store")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function listNames()
|
||||||
|
local aa = listItemAmounts()
|
||||||
|
local names = {}
|
||||||
|
|
||||||
|
for name, _ in pairs(aa) do
|
||||||
|
table.insert(names, name)
|
||||||
|
end
|
||||||
|
|
||||||
|
return names
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getItem(z)
|
||||||
|
return {} -- can't implement this well enough sadly, just expect every item exists
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
detectPlayerInsert = detectPlayerInsert,
|
||||||
|
sendItemAwayMultiple = sendItemAwayMultiple,
|
||||||
|
sendItemToSelf = sendItemToSelf,
|
||||||
|
getTurtleInventory = getTurtleInventory,
|
||||||
|
listItemAmounts = listItemAmounts,
|
||||||
|
listNames = listNames,
|
||||||
|
getItem = getItem,
|
||||||
|
sync = sync,
|
||||||
|
run = run
|
||||||
|
}
|
||||||
|
|
@ -48,12 +48,16 @@ local function run()
|
||||||
function()
|
function()
|
||||||
return com
|
return com
|
||||||
end,
|
end,
|
||||||
function(option)
|
function(option, key)
|
||||||
|
if not option then return end
|
||||||
|
|
||||||
|
if key == keys.enter then
|
||||||
local z = option:match("^(%S+)")
|
local z = option:match("^(%S+)")
|
||||||
if inv.getItem(z) then
|
if inv.getItem(z) then
|
||||||
inv.sendItemToSelf(z)
|
inv.sendItemToSelf(z)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
)
|
)
|
||||||
|
|
||||||
PrimeUI.inputBox(win, 2, 2, w - 2, function(data)
|
PrimeUI.inputBox(win, 2, 2, w - 2, function(data)
|
||||||
|
|
@ -79,6 +83,17 @@ local function run()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
PrimeUI.addTask(function()
|
||||||
|
while true do
|
||||||
|
local _, osTimer = os.pullEvent("timer")
|
||||||
|
if osTimer == timer then
|
||||||
|
com = getFiltered()
|
||||||
|
updateEntries()
|
||||||
|
timer = os.startTimer(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
PrimeUI.run()
|
PrimeUI.run()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue