storage-solution/src/lib/hmac-pbkdf2-aead.lua

208 lines
9.3 KiB
Lua
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}