Compare commits
23 commits
Author | SHA1 | Date | |
---|---|---|---|
|
3901045eef | ||
|
02a1d78578 | ||
|
7e7591d28f | ||
|
931c254523 | ||
|
3f090f59fe | ||
|
2893dff342 | ||
|
944ad4d679 | ||
|
58cff5be78 | ||
|
71552d74f1 | ||
|
d1efd74ad7 | ||
|
fd0d1d22ee | ||
|
95340ad79d | ||
|
9ed0df61c4 | ||
|
f0f9c4b940 | ||
|
91072fa555 | ||
|
52307c723f | ||
|
11ca366908 | ||
|
9d7943920f | ||
|
91e6f32894 | ||
|
0604359dbb | ||
|
3da91cf3a2 | ||
|
8d77e6597c | ||
|
5c615a14d3 |
20
README.md
20
README.md
|
@ -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()
|
||||
```
|
||||
|
|
|
@ -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
13
ccryptolib/init.lua
Normal 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");
|
||||
}
|
|
@ -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 == ">")
|
||||
|
|
|
@ -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
169
ccryptolib/sha512.lua
Normal 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,
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
3
example/encrypted_chat/README.md
Normal file
3
example/encrypted_chat/README.md
Normal 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
|
205
example/encrypted_chat/encchat.lua
Normal file
205
example/encrypted_chat/encchat.lua
Normal 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)
|
4
example/secure_login/README.md
Normal file
4
example/secure_login/README.md
Normal 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.
|
28
example/secure_login/passwd.lua
Normal file
28
example/secure_login/passwd.lua
Normal 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")
|
46
example/secure_login/startup.lua
Normal file
46
example/secure_login/startup.lua
Normal 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()
|
28
example/secure_login/useradd.lua
Normal file
28
example/secure_login/useradd.lua
Normal 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
18
install.lua
Normal 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")
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue