Compare commits

..

12 commits

Author SHA1 Message Date
Christopher Cookman 3901045eef
Add some silly examples 2024-10-28 00:36:51 -06:00
Christopher Cookman 02a1d78578
Copy sha512 to main lib for general use 2024-10-28 00:23:43 -06:00
Christopher Cookman 7e7591d28f
Update README 2024-10-27 17:55:35 -06:00
Christopher Cookman 931c254523
Whoops 2024-10-27 16:37:17 -06:00
Christopher Cookman 3f090f59fe
Bwuh 2024-10-27 16:36:35 -06:00
Christopher Cookman 2893dff342
add init script 2024-10-27 16:35:26 -06:00
Christopher Cookman 944ad4d679
Test install script 2024-10-27 16:31:05 -06:00
Miguel Oliveira 58cff5be78
Merge pull request #3 from powerboat9/patch-1
Make maskS from x25519c.lua local
2024-05-31 23:14:07 -03:00
Owen Avery 71552d74f1
Make maskS from x25519c.lua local 2024-05-31 22:08:23 -04:00
Miguel Oliveira d1efd74ad7
Fix unsupported ingame epoch calls in random.lua 2024-05-19 12:24:33 -03:00
Miguel Oliveira fd0d1d22ee Merge branch 'develop' 2024-05-12 19:02:12 -03:00
Miguel Oliveira 9ed0df61c4 Merge branch 'develop' 2023-10-29 19:42:43 -03:00
12 changed files with 527 additions and 6 deletions

View file

@ -1,6 +1,12 @@
# CCryptoLib
An integrated collection of cryptographic primitives written in Lua using the ComputerCraft system API.
## Installation
A simple install script is provided, just run the following command: `wget run https://git.chrischro.me/ChrisChrome/ccryptolib/raw/branch/main/install.lua`
## Initialization
An `init.lua` is provided to allow easier access to all modules, use `crypto = require("ccryptolib")` in your script to use the module!
## Initializing the Random Number Generator
All functions that take secret input may query the library's random generator,
`ccryptolib.random`. CC doesn't have high-quality entropy sources, so instead of
@ -11,7 +17,7 @@ hoping for the best like other libraries do, CCryptoLib shifts that burden into
If you trust the tmpim Krist node, you can fetch a socket token and use it for
initialization:
```lua
local random = require "ccryptolib.random"
local crypto = require("ccryptolib")
-- Fetch a WebSocket token.
local postHandle = assert(http.post("https://krist.dev/ws/start", ""))
@ -19,7 +25,7 @@ local data = textutils.unserializeJSON(postHandle.readAll())
postHandle.close()
-- Initialize the generator using the given URL.
random.init(data.url)
crypto.random.init(data.url)
-- Be polite and actually open the socket too.
http.websocket(data.url).close()
@ -29,6 +35,6 @@ http.websocket(data.url).close()
As of v1.2.0, you can also initialize the generator using VM instruction timing noise.
See the `random.initWithTiming` method for security risks of taking this approach.
```lua
local random = require "ccryptolib.random"
random.initWithTiming()
local crypto = require("ccryptolib")
crypto.random.initWithTiming()
```

13
ccryptolib/init.lua Normal file
View file

@ -0,0 +1,13 @@
return {
random = require("ccryptolib.random");
aead = require("ccryptolib.aead");
blake3 = require("ccryptolib.blake3");
chacha20 = require("ccryptolib.chacha20");
ed25519 = require("ccryptolib.ed25519");
poly1305 = require("ccryptolib.poly1305");
sha256 = require("ccryptolib.sha256");
sha512 = require("ccryptolib.sha512");
util = require("ccryptolib.util");
x25519 = require("ccryptolib.x25519");
x25519c = require("ccryptolib.x25519c");
}

View file

@ -9,7 +9,8 @@ local lassert = util.lassert
local ctx = {
"ccryptolib 2023-04-11T19:43Z random.lua initialization context",
os.epoch("utc"),
os.epoch("ingame"),
os.day(),
os.time(),
math.random(0, 2 ^ 24 - 1),
math.random(0, 2 ^ 24 - 1),
tostring({}),

169
ccryptolib/sha512.lua Normal file
View file

@ -0,0 +1,169 @@
--- The SHA512 cryptographic hash function.
local expect = require "cc.expect".expect
local packing = require "ccryptolib.internal.packing"
local shl = bit32.lshift
local shr = bit32.rshift
local bxor = bit32.bxor
local bnot = bit32.bnot
local band = bit32.band
local p1x16, fmt1x16 = packing.compilePack(">I16")
local p16x4, fmt16x4 = packing.compilePack(">I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4")
local u32x4, fmt32x4 = packing.compileUnpack(">I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4")
local function carry64(a1, a0)
local r0 = a0 % 2 ^ 32
a1 = a1 + (a0 - r0) / 2 ^ 32
return a1 % 2 ^ 32, r0
end
local K = {
0x428a2f98, 0xd728ae22, 0x71374491, 0x23ef65cd, 0xb5c0fbcf, 0xec4d3b2f,
0xe9b5dba5, 0x8189dbbc, 0x3956c25b, 0xf348b538, 0x59f111f1, 0xb605d019,
0x923f82a4, 0xaf194f9b, 0xab1c5ed5, 0xda6d8118, 0xd807aa98, 0xa3030242,
0x12835b01, 0x45706fbe, 0x243185be, 0x4ee4b28c, 0x550c7dc3, 0xd5ffb4e2,
0x72be5d74, 0xf27b896f, 0x80deb1fe, 0x3b1696b1, 0x9bdc06a7, 0x25c71235,
0xc19bf174, 0xcf692694, 0xe49b69c1, 0x9ef14ad2, 0xefbe4786, 0x384f25e3,
0x0fc19dc6, 0x8b8cd5b5, 0x240ca1cc, 0x77ac9c65, 0x2de92c6f, 0x592b0275,
0x4a7484aa, 0x6ea6e483, 0x5cb0a9dc, 0xbd41fbd4, 0x76f988da, 0x831153b5,
0x983e5152, 0xee66dfab, 0xa831c66d, 0x2db43210, 0xb00327c8, 0x98fb213f,
0xbf597fc7, 0xbeef0ee4, 0xc6e00bf3, 0x3da88fc2, 0xd5a79147, 0x930aa725,
0x06ca6351, 0xe003826f, 0x14292967, 0x0a0e6e70, 0x27b70a85, 0x46d22ffc,
0x2e1b2138, 0x5c26c926, 0x4d2c6dfc, 0x5ac42aed, 0x53380d13, 0x9d95b3df,
0x650a7354, 0x8baf63de, 0x766a0abb, 0x3c77b2a8, 0x81c2c92e, 0x47edaee6,
0x92722c85, 0x1482353b, 0xa2bfe8a1, 0x4cf10364, 0xa81a664b, 0xbc423001,
0xc24b8b70, 0xd0f89791, 0xc76c51a3, 0x0654be30, 0xd192e819, 0xd6ef5218,
0xd6990624, 0x5565a910, 0xf40e3585, 0x5771202a, 0x106aa070, 0x32bbd1b8,
0x19a4c116, 0xb8d2d0c8, 0x1e376c08, 0x5141ab53, 0x2748774c, 0xdf8eeb99,
0x34b0bcb5, 0xe19b48a8, 0x391c0cb3, 0xc5c95a63, 0x4ed8aa4a, 0xe3418acb,
0x5b9cca4f, 0x7763e373, 0x682e6ff3, 0xd6b2b8a3, 0x748f82ee, 0x5defb2fc,
0x78a5636f, 0x43172f60, 0x84c87814, 0xa1f0ab72, 0x8cc70208, 0x1a6439ec,
0x90befffa, 0x23631e28, 0xa4506ceb, 0xde82bde9, 0xbef9a3f7, 0xb2c67915,
0xc67178f2, 0xe372532b, 0xca273ece, 0xea26619c, 0xd186b8c7, 0x21c0c207,
0xeada7dd6, 0xcde0eb1e, 0xf57d4f7f, 0xee6ed178, 0x06f067aa, 0x72176fba,
0x0a637dc5, 0xa2c898a6, 0x113f9804, 0xbef90dae, 0x1b710b35, 0x131c471b,
0x28db77f5, 0x23047d84, 0x32caab7b, 0x40c72493, 0x3c9ebe0a, 0x15c9bebc,
0x431d67c4, 0x9c100d4c, 0x4cc5d4be, 0xcb3e42b6, 0x597f299c, 0xfc657e2a,
0x5fcb6fab, 0x3ad6faec, 0x6c44198c, 0x4a475817,
}
--- Hashes data bytes using SHA512.
--- @param data string The input data.
--- @return string hash The 64-byte hash value.
local function digest(data)
expect(1, data, "string")
-- Pad input.
local bitlen = #data * 8
local padlen = -(#data + 17) % 128
data = data .. "\x80" .. ("\0"):rep(padlen) .. p1x16(fmt1x16, bitlen)
-- Initialize state.
local h01, h00 = 0x6a09e667, 0xf3bcc908
local h11, h10 = 0xbb67ae85, 0x84caa73b
local h21, h20 = 0x3c6ef372, 0xfe94f82b
local h31, h30 = 0xa54ff53a, 0x5f1d36f1
local h41, h40 = 0x510e527f, 0xade682d1
local h51, h50 = 0x9b05688c, 0x2b3e6c1f
local h61, h60 = 0x1f83d9ab, 0xfb41bd6b
local h71, h70 = 0x5be0cd19, 0x137e2179
-- Digest.
for i = 1, #data, 128 do
local w = {u32x4(fmt32x4, data, i)}
-- Message schedule.
for j = 33, 160, 2 do
local wf1, wf0 = w[j - 30], w[j - 29]
local t1 = shr(wf1, 1) + shl(wf0, 31)
local t0 = shr(wf0, 1) + shl(wf1, 31)
local u1 = shr(wf1, 8) + shl(wf0, 24)
local u0 = shr(wf0, 8) + shl(wf1, 24)
local v1 = shr(wf1, 7)
local v0 = shr(wf0, 7) + shl(wf1, 25)
local w21, w20 = w[j - 4], w[j - 3]
local w1 = shr(w21, 19) + shl(w20, 13)
local w0 = shr(w20, 19) + shl(w21, 13)
local x0 = shr(w21, 29) + shl(w20, 3)
local x1 = shr(w20, 29) + shl(w21, 3)
local y1 = shr(w21, 6)
local y0 = shr(w20, 6) + shl(w21, 26)
local r1, r0 =
w[j - 32] + bxor(t1, u1, v1) + w[j - 14] + bxor(w1, x1, y1),
w[j - 31] + bxor(t0, u0, v0) + w[j - 13] + bxor(w0, x0, y0)
w[j], w[j + 1] = carry64(r1, r0)
end
-- Block function.
local a1, a0 = h01, h00
local b1, b0 = h11, h10
local c1, c0 = h21, h20
local d1, d0 = h31, h30
local e1, e0 = h41, h40
local f1, f0 = h51, h50
local g1, g0 = h61, h60
local h1, h0 = h71, h70
for j = 1, 160, 2 do
local t1 = shr(e1, 14) + shl(e0, 18)
local t0 = shr(e0, 14) + shl(e1, 18)
local u1 = shr(e1, 18) + shl(e0, 14)
local u0 = shr(e0, 18) + shl(e1, 14)
local v0 = shr(e1, 9) + shl(e0, 23)
local v1 = shr(e0, 9) + shl(e1, 23)
local s11 = bxor(t1, u1, v1)
local s10 = bxor(t0, u0, v0)
local ch1 = bxor(band(e1, f1), band(bnot(e1), g1))
local ch0 = bxor(band(e0, f0), band(bnot(e0), g0))
local temp11 = h1 + s11 + ch1 + K[j] + w[j]
local temp10 = h0 + s10 + ch0 + K[j + 1] + w[j + 1]
local w1 = shr(a1, 28) + shl(a0, 4)
local w0 = shr(a0, 28) + shl(a1, 4)
local x0 = shr(a1, 2) + shl(a0, 30)
local x1 = shr(a0, 2) + shl(a1, 30)
local y0 = shr(a1, 7) + shl(a0, 25)
local y1 = shr(a0, 7) + shl(a1, 25)
local s01 = bxor(w1, x1, y1)
local s00 = bxor(w0, x0, y0)
local maj1 = bxor(band(a1, b1), band(a1, c1), band(b1, c1))
local maj0 = bxor(band(a0, b0), band(a0, c0), band(b0, c0))
local temp21 = s01 + maj1
local temp20 = s00 + maj0
h1 = g1 h0 = g0
g1 = f1 g0 = f0
f1 = e1 f0 = e0
e1, e0 = carry64(d1 + temp11, d0 + temp10)
d1 = c1 d0 = c0
c1 = b1 c0 = b0
b1 = a1 b0 = a0
a1, a0 = carry64(temp11 + temp21, temp10 + temp20)
end
h01, h00 = carry64(h01 + a1, h00 + a0)
h11, h10 = carry64(h11 + b1, h10 + b0)
h21, h20 = carry64(h21 + c1, h20 + c0)
h31, h30 = carry64(h31 + d1, h30 + d0)
h41, h40 = carry64(h41 + e1, h40 + e0)
h51, h50 = carry64(h51 + f1, h50 + f0)
h61, h60 = carry64(h61 + g1, h60 + g0)
h71, h70 = carry64(h71 + h1, h70 + h0)
end
return p16x4(fmt16x4,
h01, h00, h11, h10, h21, h20, h31, h30,
h41, h40, h51, h50, h61, h60, h71, h70
)
end
return {
digest = digest,
}

View file

@ -22,7 +22,7 @@ end
--- Masks a signature secret key.
--- @param sk string A random 32-byte Edwards25519 secret key.
--- @return string msk A masked secret key.
function maskS(sk)
local function maskS(sk)
expect(1, sk, "string")
lassert(#sk == 32, "secret key length must be 32", 2)
return mask(sha512.digest(sk):sub(1, 32))

View file

@ -0,0 +1,3 @@
# E2EE P2P RedNet chat.
This ones a hot mess, apologies for that.
Uses ed25519 keys to get a shared key that isn't transmitted, used to encrypt messages with chacha20

View file

@ -0,0 +1,205 @@
term.clear()
term.setCursorPos(1, 1)
-- Find modems
for _, sModem in ipairs(peripheral.getNames()) do
if peripheral.getType(sModem) == "modem" then rednet.open(sModem) end
end
local knownHosts = settings.get("knownhosts")
if knownHosts == nil then
settings.set("knownhosts", {})
settings.save()
end
local isClient = true
local PK = nil
local crypto = require("ccryptolib")
crypto.random.initWithTiming()
local to = arg[1] -- Who we're trying to talk to (computer id)
write("Your Nickname: ")
local from = read()
if to == nil then print("You need to provide a computer ID!") end
local hexPrivate = settings.get("keys.private")
local hexPublic = settings.get("keys.public")
if hexPrivate == nil then
settings.set("keys.private", crypto.util.toHex(crypto.random.random(32)))
hexPrivate = settings.get("keys.private")
settings.save()
end
if hexPublic == nil then
settings.set("keys.public", crypto.util.toHex(crypto.x25519.publicKey(crypto.util.fromHex(settings.get("keys.private")))))
hexPublic = settings.get("keys.public")
settings.save()
end
local privateKey = crypto.util.fromHex(hexPrivate)
local publicKey = crypto.util.fromHex(hexPublic)
local termX, termY = term.getSize()
function string.starts(String, Start)
return string.sub(String, 1, string.len(Start)) == Start
end
function rx()
while true do
sleep(0)
sender, rawData = rednet.receive("encChat")
if sender ~= tonumber(to) then return end
data = textutils.unserialise(crypto.chacha20.crypt(PK, rawData.nonce, rawData.data))
if not data.test then return end -- Failed decrypt, possible bad key exchange?
term.setCursorPos(1, termY)
term.clearLine()
print(data.from .. ":" .. data.msg)
term.blit("> ", "30", "ff")
term.setCursorPos(3, termY)
end
end
function tx()
while true do
sleep(0)
term.setCursorPos(1, termY)
term.blit("> ", "30", "ff")
local msg = read()
if (string.starts(msg, "/")) then
-- Command handler
msg = string.lower(msg)
if msg == ("/q" or "/quit" or "/exit") then
error("Goodbye!")
end
else
term.scroll(-1)
term.clearLine()
write(from .. ":" .. msg)
term.scroll(1)
nonce = crypto.random.random(12)
msgData = {
timestamp = os.epoch("utc"),
msg = msg,
from = from,
test = true
}
rednet.send(tonumber(to), {
nonce = nonce,
data = crypto.chacha20
.crypt(PK, nonce, textutils.serialise(msgData))
}, "encChat")
end
end
end
function ping()
local fails = 0
if isClient then
while true do
sleep(1)
nonce = crypto.random.random(12)
msgData = {timestamp = os.epoch("utc"), test = true}
rednet.send(tonumber(to), {
nonce = nonce,
data = crypto.chacha20
.crypt(PK, nonce, textutils.serialise(msgData))
}, "encPing")
-- Wait for reply
sender, reply = rednet.receive("encPingReply", 5)
if fails > 2 then
error(
"No response from other user, They likely closed their client exiting...")
end
if sender == nil or data == nil then
fails = fails + 1
elseif crypto.chacha20.crypt(PK, reply.nonce, reply.data) ~=
tostring(msgData.timestamp) then
error("Invalid reply from ping, exiting for security.")
else
fails = 0
end
end
else
while true do
sleep(1)
sender, data = rednet.receive("encPing", 10)
if fails > 1 then
error("No response from other user, They likely closed their client exiting...")
end
if sender == nil or data == nil then
fails = fails + 1
elseif sender == tonumber(to) then
timestamp = textutils.unserialise(crypto.chacha20.crypt(PK, data.nonce, data.data)).timestamp
if os.epoch("utc") > timestamp then
fails = 0
local nonce = crypto.random.random(12)
rednet.send(sender, {
nonce = nonce,
data = crypto.chacha20
.crypt(PK, nonce, tostring(timestamp))
}, "encPingReply")
end
end
end
end
end
function exchange()
print("sending ping to " .. to)
rednet.send(tonumber(to), publicKey, "keyExClient")
sender, data = rednet.receive("keyEx", 1)
if sender ~= tonumber(to) then -- Switch to server
print("no response from " .. to .. ". listening for ping...")
isClient = false
while true do
sender, data = rednet.receive("keyExClient")
if sender == tonumber(to) then
print("ping from " .. sender .. ". sending and generating keys...")
rednet.send(tonumber(to), publicKey, "keyEx")
if knownHosts[tonumber(to)] == nil then
write("remote pubkey is " .. crypto.util.toHex(data) ..". Do you trust it? (Y/N)")
local trust = string.lower(read())
if trust == "y" then
knownHosts[tonumber(to)] = data
settings.set("knownhosts", knownHosts)
settings.save()
PK = crypto.x25519.exchange(privateKey, data)
else
error("Exiting...")
end
elseif knownHosts[tonumber(to)] ~= data then
error("==DANGER== Remote pubkey differs from previous interaction!")
else
PK = crypto.x25519.exchange(privateKey, data)
end
break
end
end
else
print("response from " .. to)
if knownHosts[tonumber(to)] == nil then
write("remote pubkey is " .. crypto.util.toHex(data) ..". Do you trust it? (Y/N)")
local trust = string.lower(read())
if trust == "y" then
knownHosts[tonumber(to)] = data
settings.set("knownhosts", knownHosts)
settings.save()
PK = crypto.x25519.exchange(privateKey, data)
else
error("Exiting...")
end
elseif knownHosts[tonumber(to)] ~= data then
error("==DANGER== Remote pubkey differs from previous interaction!")
else
PK = crypto.x25519.exchange(privateKey, data)
end
PK = crypto.x25519.exchange(privateKey, data)
end
term.clear()
term.setCursorPos(1,1)
print("==BEGIN ENCRYPTED CONVERSATION==")
end
exchange()
parallel.waitForAll(rx, tx, ping)

View file

@ -0,0 +1,4 @@
# SHA-512 hashed login system
This is a really simple setup to secure a computer in computercraft from them pesky fuckers in your server :P
Multi user support, but no separation of files or any file permissions at all for that matter. i.e if someone can log in, they can disable the login and change passwords, beware.

View file

@ -0,0 +1,28 @@
local username = arg[1] or _G.loggedInUser
local passwdData = settings.get("passwd")
if passwdData[username] == nil then
error("passwd: user '" .. username .. "' does not exist")
end
local crypto = require("ccryptolib")
crypto.random.initWithTiming()
print("Changing password for " .. username .. ".")
write("New password: ")
local pwd1 = crypto.sha512.digest(read(""))
write("Retype new password: ")
local pwd2 = crypto.sha512.digest(read(""))
if pwd1 ~= pwd2 then
error("Sorry, passwords do not match.")
end
if pwd1 == passwdData[username] then
error("The password has not been changed.")
end
passwdData[username] = crypto.util.toHex(pwd1)
settings.set("passwd", passwdData)
settings.save()
print("passwd: password updated successfully")

View file

@ -0,0 +1,46 @@
-- No term for you :)
Original_pullEvent = os.pullEvent
os.pullEvent = os.pullEventRaw
settings.set("shell.allow_disk_startup", false) -- Prevent disk startup bypass
-- Header stuff
local hostname = os.getComputerLabel() or "craftos"
term.clear()
term.setCursorPos(1,1)
write(_G._ENV._HOST .. " " .. hostname)
-- Load crypto library (slow, so we do it after everything else)
Crypto = require("ccryptolib")
Crypto.random.initWithTiming()
function checkCreds(username, hashed)
if Crypto.util.fromHex(settings.get("passwd")[username]) == hashed then
return true
else
return false
end
end
function login()
print()
write(hostname .. " login: ")
local username = read()
write("Password: ")
local passwordHash = Crypto.sha512.digest(read("")) -- Hash immidiately so password is not stored plaintext in memory
if checkCreds(username, passwordHash) then
print("Welcome ".. username)
_G.loggedInUser = username
else
print("Login incorrect")
login()
end
end
if not settings.get("passwd") then
print()
print("WARNING: passwd file does not exist, generating default login root:toor")
settings.set("passwd", { ["root"] = Crypto.util.toHex(Crypto.sha512.digest("toor")) })
settings.save()
end
login()

View file

@ -0,0 +1,28 @@
local username = arg[1]
local passwdData = settings.get("passwd")
if passwdData[username] then
error("useradd: user '" .. username .."' already exists")
end
local crypto = require("ccryptolib")
crypto.random.initWithTiming()
print("Changing password for " .. username .. ".")
write("New password: ")
local pwd1 = crypto.sha512.digest(read(""))
write("Retype new password: ")
local pwd2 = crypto.sha512.digest(read(""))
if pwd1 ~= pwd2 then
error("Sorry, passwords do not match.")
end
if pwd1 == passwdData[username] then
error("The password has not been changed.")
end
passwdData[username] = crypto.util.toHex(pwd1)
settings.set("passwd", passwdData)
settings.save()
print("useradd: password updated successfully")

18
install.lua Normal file
View file

@ -0,0 +1,18 @@
fs.makeDir("/ccryptolib") -- Make folder structure
function downloadDir(dir)
local data = textutils.unserialiseJSON(http.get("https://git.chrischro.me/api/v1/repos/ChrisChrome/ccryptolib/contents/" .. dir).readAll())
for _,entry in ipairs(data) do
if entry.type == "file" then -- We're gonna download it
print("Downloading " .. entry.name)
local file = http.get(entry.download_url).readAll()
local newFile = fs.open("/" .. entry.path, "w")
newFile.write(file)
newFile.close()
elseif entry.type == "dir" then
downloadDir(entry.path)
end
end
end
downloadDir("ccryptolib")