Compare commits

...

23 commits
v1.0.0 ... main

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 95340ad79d Add timing based generator initialization 2024-05-12 18:57:43 -03:00
Miguel Oliveira 9ed0df61c4 Merge branch 'develop' 2023-10-29 19:42:43 -03:00
Miguel Oliveira f0f9c4b940 Merge branch 'main' into develop 2023-10-29 19:38:45 -03:00
Miguel Oliveira 91072fa555 Stabilize x25519c.lua 2023-10-29 19:37:38 -03:00
Miguel Oliveira 52307c723f
Merge pull request #2 from SuoDizzy/patch-1
Function Description mismatching actual code
2023-09-22 08:26:58 -03:00
SuoDizzy 11ca366908
Function Description mismatching actual code
The autocomplete description had the parameters mismatching with the actually needed ones, you may choose to edit the parameter order in the functions actual code if you had wanted it another way, this commit just changes the description to match with the code.
2023-07-29 12:19:46 +03:00
Miguel Oliveira 9d7943920f
Merge pull request #1 from migeyel/develop
Various fixes
2023-07-18 21:14:49 -03:00
Miguel Oliveira 91e6f32894 Fix "cryptolib" typo 2023-07-18 21:12:20 -03:00
Miguel Oliveira 0604359dbb Loosen integral limits on packing.lua 2023-07-18 21:12:00 -03:00
Miguel Oliveira 3da91cf3a2 Fix random.random
Whoops.
2023-06-11 12:01:05 -03:00
Miguel Oliveira 8d77e6597c Check argument types in random.mix 2023-06-09 21:23:02 -03:00
Miguel Oliveira 5c615a14d3 Fix random.random erroring with a negative length
Calling random.random(-1) will return an empty string and set the state
to a 31-byte string. This makes any further call in the module error.
2023-06-09 21:22:25 -03:00
17 changed files with 602 additions and 102 deletions

View file

@ -1,16 +1,23 @@
# 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
hoping for the best like other libraries do, CCryptoLib shifts that burden into
*you!*
### Initializing using a Trusted Web Source
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", ""))
@ -18,11 +25,16 @@ 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()
```
Otherwise, you will need to find another high-quality random entropy source to
initialize the generator. **DO NOT INITIALIZE USING MATH.RANDOM.**
### Initializing using VM Instruction Counting
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 crypto = require("ccryptolib")
crypto.random.initWithTiming()
```

View file

@ -50,9 +50,9 @@ end
--- Decrypts a message.
--- @param key string The key used on encryption.
--- @param nonce string The nonce used on encryption.
--- @param tag string The authentication tag returned on encryption.
--- @param ciphertext string The ciphertext to be decrypted.
--- @param aad string The arbitrary associated data used on encryption.
--- @param tag string The authentication tag returned on encryption.
--- @param rounds number The number of rounds used on encryption.
--- @return string? msg The decrypted plaintext. Or nil on auth failure.
local function decrypt(key, nonce, tag, ciphertext, aad, rounds)

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

@ -95,7 +95,7 @@ if not string.pack or pcall(string.dump, string.pack) then
local w = {}
for i in fmt:gmatch("I([%d]+)") do
local n = tonumber(i) or 4
assert(n > 0 and n <= 4, "integral size out of limits")
assert(n > 0 and n <= 16, "integral size out of limits")
w[#w + 1] = n
end
return fn(w, e == ">")

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({}),
@ -27,9 +28,59 @@ local function init(seed)
initialized = true
end
--- Returns whether the generator has been initialized or not.
--- @return boolean
local function isInit()
return initialized
end
--- Initializes the generator using VM instruction timing noise.
---
--- This function counts how many instructions the VM can execute within a single
--- millisecond, and mixes the lower bits of these values into the generator state.
--- The current implementation collects data for 512 ms and takes the lower 8 bits from
--- each count.
---
--- Compared to fetching entropy from a trusted web source, this approach is riskier but
--- more convenient. The factors that influence instruction timing suggest that this
--- seed is unpredictable for other players, but this assumption might turn out to be
--- untrue.
local function initWithTiming()
assert(os.epoch("utc") ~= 0)
local f = assert(load("local e=os.epoch return{" .. ("e'utc',"):rep(256) .. "}"))
do -- Warmup.
local t = f()
while t[256] - t[1] > 1 do t = f() end
end
-- Fill up the buffer.
local buf = {}
for i = 1, 512 do
local t = f()
while t[256] == t[1] do t = f() end
for j = 1, 256 do
if t[j] ~= t[1] then
buf[i] = j - 1
break
end
end
end
-- Perform a histogram check to catch faulty os.epoch implementations.
local hist = {}
for i = 0, 255 do hist[i] = 0 end
for i = 1, #buf do hist[buf[i]] = hist[buf[i]] + 1 end
for i = 0, 255 do assert(hist[i] < 20) end
init(string.char(table.unpack(buf)))
end
--- Mixes extra entropy into the generator state.
--- @param data string The additional entropy to mix.
local function mix(data)
expect(1, data, "string")
state = blake3.digestKeyed(state, data)
end
@ -39,7 +90,7 @@ end
local function random(len)
expect(1, len, "number")
lassert(initialized, "attempt to use an uninitialized random generator", 2)
local msg = ("\0"):rep(len + 32)
local msg = ("\0"):rep(math.max(len, 0) + 32)
local nonce = ("\0"):rep(12)
local out = chacha20.crypt(state, nonce, msg, 8, 0)
state = out:sub(1, 32)
@ -48,6 +99,8 @@ end
return {
init = init,
isInit = isInit,
initWithTiming = initWithTiming,
mix = mix,
random = random,
}

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

@ -1,7 +1,7 @@
--- General utilities for handling byte strings.
local expect = require "cc.expect".expect
local random = require "cryptolib.random"
local random = require "ccryptolib.random"
local poly1305 = require "ccryptolib.poly1305"
--- Returns the hexadecimal version of a string.

View file

@ -26,21 +26,7 @@ local function exchange(sk, pk)
return c25.encode(c25.scale(c25.ladder8(c25.decode(pk), util.bits8(sk))))
end
--- Performs the key exchange, but decoding the public key as an Edwards25519
--- point, using the birational map.
--- @param sk string A Curve25519 secret key
--- @param pk string An Edwards25519 public key, usually derived from someone else's secret key.
--- @return string ss The 32-byte shared secret between both keys.
local function exchangeEd(sk, pk)
expect(1, sk, "string")
lassert(#sk == 32, "secret key length must be 32", 2)
expect(2, pk, "string")
lassert(#pk == 32, "public key length must be 32", 2) --- @cast pk String32
return c25.encode(c25.scale(c25.ladder8(c25.decodeEd(pk), util.bits8(sk))))
end
return {
publicKey = publicKey,
exchange = exchange,
_EXPERIMENTAL_exchangeEd = exchangeEd,
}

View file

@ -3,14 +3,13 @@ local lassert = require "ccryptolib.internal.util".lassert
local fq = require "ccryptolib.internal.fq"
local fp = require "ccryptolib.internal.fp"
local c25 = require "ccryptolib.internal.curve25519"
local ed = require "ccryptolib.internal.edwards25519"
local sha512 = require "ccryptolib.internal.sha512"
local random = require "ccryptolib.random"
--- Masks an exchange secret key.
--- @param sk string A random 32-byte Curve25519 secret key.
--- @return string msk A masked secret key.
local function maskX(sk)
local function mask(sk)
expect(1, sk, "string")
lassert(#sk == 32, "secret key length must be 32", 2)
local mask = random.random(32)
@ -23,10 +22,10 @@ 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 maskX(sha512.digest(sk):sub(1, 32))
return mask(sha512.digest(sk):sub(1, 32))
end
--- Rerandomizes the masking on a masked key.
@ -115,24 +114,12 @@ end
--- Returns the X25519 public key of this masked key.
--- @param msk string A masked secret key.
local function publicKeyX(msk)
local function publicKey(msk)
expect(1, msk, "string")
lassert(#msk == 64, "masked secret key length must be 64", 2)
return (exchangeOnPoint(msk, c25.G))
end
--- Returns the Ed25519 public key of this masked key.
--- @param msk string A masked secret key.
--- @return string pk The Ed25519 public key matching this masked key.
local function publicKeyS(msk)
expect(1, msk, "string")
lassert(#msk == 64, "masked secret key length must be 64", 2)
local xr = fq.decode(msk:sub(1, 32))
local r = fq.decodeClamped(msk:sub(33))
local y = ed.add(ed.mulG(fq.bits(xr)), ed.niels(ed.mulG(fq.bits(r))))
return ed.encode(ed.scale(y))
end
--- Performs a double key exchange.
---
--- Returns 0 if the input public key has small order or if it isn't in the base
@ -146,7 +133,7 @@ end
--- @param pk string An X25519 public key.
--- @return string sss The shared secret between the public key and the static half of the masked key.
--- @return string sse The shared secret betwen the public key and the ephemeral half of the masked key.
local function exchangeX(sk, pk)
local function exchange(sk, pk)
expect(1, sk, "string")
lassert(#sk == 64, "masked secret key length must be 64", 2)
expect(2, pk, "string")
@ -154,62 +141,10 @@ local function exchangeX(sk, pk)
return exchangeOnPoint(sk, c25.decode(pk))
end
--- Performs an exchange against an Ed25519 key.
---
--- This is done by converting the key into X25519 before passing it to the
--- regular exchange. Using this function on the result of @{signaturePk} leads
--- to the same value as using @{exchange} on the result of @{exchangePk}.
---
--- @param sk string A masked secret key.
--- @param pk string An Ed25519 public key.
--- @return string sss The shared secret between the public key and the static half of the masked key.
--- @return string sse The shared secret betwen the public key and the ephemeral half of the masked key.
local function exchangeS(sk, pk)
expect(1, sk, "string")
lassert(#sk == 64, "masked secret key length must be 64", 2)
expect(2, pk, "string")
lassert(#pk == 32, "public key length must be 32", 2) --- @cast pk String32
return exchangeOnPoint(sk, c25.decodeEd(pk))
end
--- Signs a message using Ed25519.
--- @param sk string A masked secret key.
--- @param pk string The Ed25519 public key matching the secret key.
--- @param msg string A message to sign.
--- @return string sig The signature on the message.
local function sign(sk, pk, msg)
expect(1, sk, "string")
lassert(#sk == 64, "masked secret key length must be 64", 2)
expect(2, pk, "string")
lassert(#pk == 32, "public key length must be 32", 2)
expect(3, msg, "string")
-- Secret key.
local xr = fq.decode(sk:sub(1, 32))
local r = fq.decodeClamped(sk:sub(33))
-- Commitment.
local k = fq.decodeWide(random.random(64))
local rStr = ed.encode(ed.mulG(fq.bits(k)))
-- Challenge.
local e = fq.decodeWide(sha512.digest(rStr .. pk .. msg))
-- Response.
local s = fq.add(fq.add(k, fq.mul(xr, e)), fq.mul(r, e))
local sStr = fq.encode(s)
return rStr .. sStr
end
return {
_EXPERIMENTAL_maskX = maskX,
_EXPERIMENTAL_maskS = maskS,
_EXPERIMENTAL_remask = remask,
_EXPERIMENTAL_publicKeyX = publicKeyX,
_EXPERIMENTAL_ephemeralSk = ephemeralSk,
_EXPERIMENTAL_publicKeyS = publicKeyS,
_EXPERIMENTAL_exchangeX = exchangeX,
_EXPERIMENTAL_exchangeS = exchangeS,
_EXPERIMENTAL_sign = sign,
mask = mask,
remask = remask,
publicKey = publicKey,
ephemeralSk = ephemeralSk,
exchange = exchange,
}

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")

View file

@ -9,12 +9,12 @@ local x25519c = require "ccryptolib.x25519c"
require "ccryptolib.random".init("mock initialization")
local function exchange(sk, pk)
local sk = x25519c._EXPERIMENTAL_maskX(sk)
sk = x25519c._EXPERIMENTAL_remask(sk)
return (x25519c._EXPERIMENTAL_exchangeX(sk, pk))
local sk = x25519c.mask(sk)
sk = x25519c.remask(sk)
return (x25519c.exchange(sk, pk))
end
describe("x25519c._EXPERIMENTAL_exchangeX", function()
describe("x25519c.exchange", function()
it("passes the section 5.2 test vector #1", function()
local x = util.hexcat {
"a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4",