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] 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 }