├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── Tests ├── RunTests.server.lua └── Tests │ └── ECC.spec.lua ├── default.project.json ├── foreman.toml ├── package.json ├── selene.toml ├── src ├── EllipticCurveCryptography │ ├── arith.lua │ ├── benchmark.lua │ ├── chacha20.lua │ ├── curve.lua │ ├── init.lua │ ├── modp.lua │ ├── modq.lua │ ├── random.lua │ ├── sha256.lua │ ├── testing.lua │ ├── twoPower.lua │ └── util.lua ├── client.lua ├── init.lua └── server.lua ├── stylua.toml ├── tests.project.json ├── wally.lock └── wally.toml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: boatbomber 2 | patreon: boatbomberrblx 3 | custom: ['https://www.buymeacoffee.com/boatbomberrblx'] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project build files 2 | /*.rbxl 3 | /*.rbxlx 4 | /*.rbxm 5 | /*.rbxmx 6 | 7 | DevPackages/* 8 | Packages/* 9 | 10 | # Roblox Studio lock files 11 | /*.rbxlx.lock 12 | /*.rbxl.lock 13 | 14 | # VS Code files 15 | .vscode/* 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 boatbomber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![itch](https://raw.githubusercontent.com/gist/csqrl/56c5f18b229ca1e61feb6eb5fb149f43/raw/itch.svg)](https://boatbomber.itch.io/encryptednet) 2 | 3 | # EncryptedNet 4 | 5 | Authenticated encryption of Roblox networking with ECDH key exchanges and ChaCha20 ciphering. 6 | 7 | This is designed to be used with [RbxNet](https://github.com/roblox-aurora/rbx-net)! A version that supports native RemoteFunction/Event instances will be coming later. 8 | 9 | ## Usage 10 | 11 | This library returns a function that wraps your remotes, like so. 12 | 13 | ```Lua 14 | -- Server 15 | local AttemptLogin = EncryptedNet(Remotes.Server:Create("AttemptLogin")) 16 | 17 | AttemptLogin:SetCallback(function(Player, password) -- Use normally 18 | -- ... 19 | end) 20 | 21 | --Client 22 | local AttemptLogin = EncryptedNet(Remotes.Client:Get("AttemptLogin")) 23 | 24 | AttemptLogin:CallServerAsync(password):andThen(function(response) -- Use normally 25 | -- ... 26 | end) 27 | ``` 28 | 29 | While there's no difference to how you write your networking, this `EncryptedNet(remote)` wrapper is actually encrypting and signing all the traffic that flows through those remotes! 30 | 31 | ![demo](https://cdn.discordapp.com/attachments/711758878995513364/945726611641233469/unknown.png) 32 | 33 | ## How EncryptedNet works 34 | 35 | When you `require()` EncryptedNet from the server, it sets up a RemoteFunction for a handshake. When you `require()` EncryptedNet from the client, it calls that handshake remote. The server and client each generate public & private keys, then send their public keys to each other. Using these keys, they perform an [Elliptic-curve Diffie–Hellman](https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman) key exchange to arrive at a shared secret that is later used to encrypt all traffic. 36 | 37 | When the server calls `:SetCallback()` on a remote, the callback is actually set to a function that receives encrypted data and a signature. This function decrypts the data with the shared secret using the [ChaCha20 cipher](https://en.wikipedia.org/wiki/Salsa20#ChaCha20_adoption) (since ChaCha20 is efficient for non-hardware applications like ours), then verifies the decrypted data using the elliptic curve signature. Once it has done that, it passes the decrypted data to your specified callback function and your code runs none the wiser. 38 | 39 | When a client calls `:CallServerAsync()` on a remote, it first takes your arguments and encrypts them with ChaCha20 and the shared secret, then creates a signature using your private key, and then sends those along. 40 | 41 | This process is similarly done around `:Connect()`, `:SendToPlayer()`, etc. All of the RbxNet API is wrapped to properly handle authenticated encryption on all of your traffic on that remote. 42 | 43 | ## Credits 44 | 45 | Absolutely *massive* shoutout to [PG23186706924](http://www.computercraft.info/forums2/index.php?/user/68959-pg23186706924/) for their [pure Lua implementation](http://www.computercraft.info/forums2/index.php?/topic/29803-elliptic-curve-cryptography/) of the elliptic curve cryptography tech that powers this library. I have ported it to Roblox and optimized it for Luau and it is wonderful. 46 | -------------------------------------------------------------------------------- /Tests/RunTests.server.lua: -------------------------------------------------------------------------------- 1 | local BoatTEST = require(workspace.DevPackages.BoatTEST) 2 | 3 | BoatTEST.run({ 4 | directories = { workspace.Tests }, 5 | }) 6 | -------------------------------------------------------------------------------- /Tests/Tests/ECC.spec.lua: -------------------------------------------------------------------------------- 1 | local BoatTEST = require(workspace.DevPackages.BoatTEST) 2 | local this = BoatTEST.this 3 | 4 | local ECC = require(workspace.ECC) 5 | 6 | return { 7 | ["Server and Client keys should not be the same"] = function(skip) 8 | -- Generate tokens 9 | local serverPrivate, serverPublic = ECC.keypair(ECC.random.random()) 10 | local clientPrivate, clientPublic = ECC.keypair(ECC.random.random()) 11 | 12 | -- Check for overlap 13 | this(serverPrivate:isEqual(clientPrivate)).equals(false) 14 | this(serverPublic:isEqual(clientPublic)).equals(false) 15 | end, 16 | } 17 | -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "EncryptedNet", 3 | "tree": { 4 | "$path": "src" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /foreman.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | selene = { source = "Kampfkarren/selene", version = "0.17.0" } 3 | stylua = { source = "JohnnyMorganz/stylua", version = "0.13.1" } 4 | rojo = { source = "rojo-rbx/rojo", version = "7.0.0" } 5 | run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" } 6 | wally = { source = "upliftgames/wally", version = "0.3.1" } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@boatbomber/encryptednet", 3 | "version": "1.0.4", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/boatbomber/EncryptedNet.git" 8 | }, 9 | "contributors": [ 10 | "boatbomber" 11 | ], 12 | "bugs": { 13 | "url": "https://github.com/boatbomber/EncryptedNet/issues" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /selene.toml: -------------------------------------------------------------------------------- 1 | std = "roblox" 2 | -------------------------------------------------------------------------------- /src/EllipticCurveCryptography/arith.lua: -------------------------------------------------------------------------------- 1 | -- Big integer arithmetic for 168-bit (and 336-bit) numbers 2 | -- Numbers are represented as little-endian tables of 24-bit integers 3 | local twoPower = require(script.Parent.twoPower) 4 | 5 | local function isEqual(a, b) 6 | return a[1] == b[1] 7 | and a[2] == b[2] 8 | and a[3] == b[3] 9 | and a[4] == b[4] 10 | and a[5] == b[5] 11 | and a[6] == b[6] 12 | and a[7] == b[7] 13 | end 14 | 15 | local function compare(a, b) 16 | for i = 7, 1, -1 do 17 | if a[i] > b[i] then 18 | return 1 19 | elseif a[i] < b[i] then 20 | return -1 21 | end 22 | end 23 | 24 | return 0 25 | end 26 | 27 | local function add(a, b) 28 | -- c7 may be greater than 2^24 before reduction 29 | local c1 = a[1] + b[1] 30 | local c2 = a[2] + b[2] 31 | local c3 = a[3] + b[3] 32 | local c4 = a[4] + b[4] 33 | local c5 = a[5] + b[5] 34 | local c6 = a[6] + b[6] 35 | local c7 = a[7] + b[7] 36 | 37 | if c1 > 0xffffff then 38 | c2 = c2 + 1 39 | c1 = c1 - 0x1000000 40 | end 41 | if c2 > 0xffffff then 42 | c3 = c3 + 1 43 | c2 = c2 - 0x1000000 44 | end 45 | if c3 > 0xffffff then 46 | c4 = c4 + 1 47 | c3 = c3 - 0x1000000 48 | end 49 | if c4 > 0xffffff then 50 | c5 = c5 + 1 51 | c4 = c4 - 0x1000000 52 | end 53 | if c5 > 0xffffff then 54 | c6 = c6 + 1 55 | c5 = c5 - 0x1000000 56 | end 57 | if c6 > 0xffffff then 58 | c7 = c7 + 1 59 | c6 = c6 - 0x1000000 60 | end 61 | 62 | return { c1, c2, c3, c4, c5, c6, c7 } 63 | end 64 | 65 | local function sub(a, b) 66 | -- c7 may be negative before reduction 67 | local c1 = a[1] - b[1] 68 | local c2 = a[2] - b[2] 69 | local c3 = a[3] - b[3] 70 | local c4 = a[4] - b[4] 71 | local c5 = a[5] - b[5] 72 | local c6 = a[6] - b[6] 73 | local c7 = a[7] - b[7] 74 | 75 | if c1 < 0 then 76 | c2 = c2 - 1 77 | c1 = c1 + 0x1000000 78 | end 79 | if c2 < 0 then 80 | c3 = c3 - 1 81 | c2 = c2 + 0x1000000 82 | end 83 | if c3 < 0 then 84 | c4 = c4 - 1 85 | c3 = c3 + 0x1000000 86 | end 87 | if c4 < 0 then 88 | c5 = c5 - 1 89 | c4 = c4 + 0x1000000 90 | end 91 | if c5 < 0 then 92 | c6 = c6 - 1 93 | c5 = c5 + 0x1000000 94 | end 95 | if c6 < 0 then 96 | c7 = c7 - 1 97 | c6 = c6 + 0x1000000 98 | end 99 | 100 | return { c1, c2, c3, c4, c5, c6, c7 } 101 | end 102 | 103 | local function rShift(a) 104 | local c1 = a[1] 105 | local c2 = a[2] 106 | local c3 = a[3] 107 | local c4 = a[4] 108 | local c5 = a[5] 109 | local c6 = a[6] 110 | local c7 = a[7] 111 | 112 | c1 = c1 / 2 113 | c1 = c1 - c1 % 1 114 | c1 = c1 + (c2 % 2) * 0x800000 115 | c2 = c2 / 2 116 | c2 = c2 - c2 % 1 117 | c2 = c2 + (c3 % 2) * 0x800000 118 | c3 = c3 / 2 119 | c3 = c3 - c3 % 1 120 | c3 = c3 + (c4 % 2) * 0x800000 121 | c4 = c4 / 2 122 | c4 = c4 - c4 % 1 123 | c4 = c4 + (c5 % 2) * 0x800000 124 | c5 = c5 / 2 125 | c5 = c5 - c5 % 1 126 | c5 = c5 + (c6 % 2) * 0x800000 127 | c6 = c6 / 2 128 | c6 = c6 - c6 % 1 129 | c6 = c6 + (c7 % 2) * 0x800000 130 | c7 = c7 / 2 131 | c7 = c7 - c7 % 1 132 | 133 | return { c1, c2, c3, c4, c5, c6, c7 } 134 | end 135 | 136 | local function addDouble(a, b) 137 | -- a and b are 336-bit integers (14 words) 138 | local c1 = a[1] + b[1] 139 | local c2 = a[2] + b[2] 140 | local c3 = a[3] + b[3] 141 | local c4 = a[4] + b[4] 142 | local c5 = a[5] + b[5] 143 | local c6 = a[6] + b[6] 144 | local c7 = a[7] + b[7] 145 | local c8 = a[8] + b[8] 146 | local c9 = a[9] + b[9] 147 | local c10 = a[10] + b[10] 148 | local c11 = a[11] + b[11] 149 | local c12 = a[12] + b[12] 150 | local c13 = a[13] + b[13] 151 | local c14 = a[14] + b[14] 152 | 153 | if c1 > 0xffffff then 154 | c2 = c2 + 1 155 | c1 = c1 - 0x1000000 156 | end 157 | if c2 > 0xffffff then 158 | c3 = c3 + 1 159 | c2 = c2 - 0x1000000 160 | end 161 | if c3 > 0xffffff then 162 | c4 = c4 + 1 163 | c3 = c3 - 0x1000000 164 | end 165 | if c4 > 0xffffff then 166 | c5 = c5 + 1 167 | c4 = c4 - 0x1000000 168 | end 169 | if c5 > 0xffffff then 170 | c6 = c6 + 1 171 | c5 = c5 - 0x1000000 172 | end 173 | if c6 > 0xffffff then 174 | c7 = c7 + 1 175 | c6 = c6 - 0x1000000 176 | end 177 | if c7 > 0xffffff then 178 | c8 = c8 + 1 179 | c7 = c7 - 0x1000000 180 | end 181 | if c8 > 0xffffff then 182 | c9 = c9 + 1 183 | c8 = c8 - 0x1000000 184 | end 185 | if c9 > 0xffffff then 186 | c10 = c10 + 1 187 | c9 = c9 - 0x1000000 188 | end 189 | if c10 > 0xffffff then 190 | c11 = c11 + 1 191 | c10 = c10 - 0x1000000 192 | end 193 | if c11 > 0xffffff then 194 | c12 = c12 + 1 195 | c11 = c11 - 0x1000000 196 | end 197 | if c12 > 0xffffff then 198 | c13 = c13 + 1 199 | c12 = c12 - 0x1000000 200 | end 201 | if c13 > 0xffffff then 202 | c14 = c14 + 1 203 | c13 = c13 - 0x1000000 204 | end 205 | 206 | return { c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14 } 207 | end 208 | 209 | local function mult(a, b, half_multiply) 210 | local a1, a2, a3, a4, a5, a6, a7 = a[1], a[2], a[3], a[4], a[5], a[6], a[7] 211 | local b1, b2, b3, b4, b5, b6, b7 = b[1], b[2], b[3], b[4], b[5], b[6], b[7] 212 | 213 | local c1 = a1 * b1 214 | local c2 = a1 * b2 + a2 * b1 215 | local c3 = a1 * b3 + a2 * b2 + a3 * b1 216 | local c4 = a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 217 | local c5 = a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 218 | local c6 = a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 219 | local c7 = a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1 220 | local c8, c9, c10, c11, c12, c13, c14 221 | if not half_multiply then 222 | c8 = a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2 223 | c9 = a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3 224 | c10 = a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4 225 | c11 = a5 * b7 + a6 * b6 + a7 * b5 226 | c12 = a6 * b7 + a7 * b6 227 | c13 = a7 * b7 228 | c14 = 0 229 | else 230 | c8 = 0 231 | end 232 | 233 | local temp 234 | temp = c1 235 | c1 = c1 % 0x1000000 236 | c2 = c2 + (temp - c1) / 0x1000000 237 | temp = c2 238 | c2 = c2 % 0x1000000 239 | c3 = c3 + (temp - c2) / 0x1000000 240 | temp = c3 241 | c3 = c3 % 0x1000000 242 | c4 = c4 + (temp - c3) / 0x1000000 243 | temp = c4 244 | c4 = c4 % 0x1000000 245 | c5 = c5 + (temp - c4) / 0x1000000 246 | temp = c5 247 | c5 = c5 % 0x1000000 248 | c6 = c6 + (temp - c5) / 0x1000000 249 | temp = c6 250 | c6 = c6 % 0x1000000 251 | c7 = c7 + (temp - c6) / 0x1000000 252 | temp = c7 253 | c7 = c7 % 0x1000000 254 | if not half_multiply then 255 | c8 = c8 + (temp - c7) / 0x1000000 256 | temp = c8 257 | c8 = c8 % 0x1000000 258 | c9 = c9 + (temp - c8) / 0x1000000 259 | temp = c9 260 | c9 = c9 % 0x1000000 261 | c10 = c10 + (temp - c9) / 0x1000000 262 | temp = c10 263 | c10 = c10 % 0x1000000 264 | c11 = c11 + (temp - c10) / 0x1000000 265 | temp = c11 266 | c11 = c11 % 0x1000000 267 | c12 = c12 + (temp - c11) / 0x1000000 268 | temp = c12 269 | c12 = c12 % 0x1000000 270 | c13 = c13 + (temp - c12) / 0x1000000 271 | temp = c13 272 | c13 = c13 % 0x1000000 273 | c14 = c14 + (temp - c13) / 0x1000000 274 | end 275 | 276 | return { c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14 } 277 | end 278 | 279 | local function square(a) 280 | -- returns a 336-bit integer (14 words) 281 | local a1, a2, a3, a4, a5, a6, a7 = a[1], a[2], a[3], a[4], a[5], a[6], a[7] 282 | 283 | local c1 = a1 * a1 284 | local c2 = a1 * a2 * 2 285 | local c3 = a1 * a3 * 2 + a2 * a2 286 | local c4 = a1 * a4 * 2 + a2 * a3 * 2 287 | local c5 = a1 * a5 * 2 + a2 * a4 * 2 + a3 * a3 288 | local c6 = a1 * a6 * 2 + a2 * a5 * 2 + a3 * a4 * 2 289 | local c7 = a1 * a7 * 2 + a2 * a6 * 2 + a3 * a5 * 2 + a4 * a4 290 | local c8 = a2 * a7 * 2 + a3 * a6 * 2 + a4 * a5 * 2 291 | local c9 = a3 * a7 * 2 + a4 * a6 * 2 + a5 * a5 292 | local c10 = a4 * a7 * 2 + a5 * a6 * 2 293 | local c11 = a5 * a7 * 2 + a6 * a6 294 | local c12 = a6 * a7 * 2 295 | local c13 = a7 * a7 296 | local c14 = 0 297 | 298 | local temp 299 | temp = c1 300 | c1 = c1 % 0x1000000 301 | c2 = c2 + (temp - c1) / 0x1000000 302 | temp = c2 303 | c2 = c2 % 0x1000000 304 | c3 = c3 + (temp - c2) / 0x1000000 305 | temp = c3 306 | c3 = c3 % 0x1000000 307 | c4 = c4 + (temp - c3) / 0x1000000 308 | temp = c4 309 | c4 = c4 % 0x1000000 310 | c5 = c5 + (temp - c4) / 0x1000000 311 | temp = c5 312 | c5 = c5 % 0x1000000 313 | c6 = c6 + (temp - c5) / 0x1000000 314 | temp = c6 315 | c6 = c6 % 0x1000000 316 | c7 = c7 + (temp - c6) / 0x1000000 317 | temp = c7 318 | c7 = c7 % 0x1000000 319 | c8 = c8 + (temp - c7) / 0x1000000 320 | temp = c8 321 | c8 = c8 % 0x1000000 322 | c9 = c9 + (temp - c8) / 0x1000000 323 | temp = c9 324 | c9 = c9 % 0x1000000 325 | c10 = c10 + (temp - c9) / 0x1000000 326 | temp = c10 327 | c10 = c10 % 0x1000000 328 | c11 = c11 + (temp - c10) / 0x1000000 329 | temp = c11 330 | c11 = c11 % 0x1000000 331 | c12 = c12 + (temp - c11) / 0x1000000 332 | temp = c12 333 | c12 = c12 % 0x1000000 334 | c13 = c13 + (temp - c12) / 0x1000000 335 | temp = c13 336 | c13 = c13 % 0x1000000 337 | c14 = c14 + (temp - c13) / 0x1000000 338 | 339 | return { c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14 } 340 | end 341 | 342 | local function encodeInt(a) 343 | local enc = table.create(21) 344 | 345 | for i = 1, 7 do 346 | local word = a[i] 347 | for _ = 1, 3 do 348 | table.insert(enc, word % 256) 349 | word = math.floor(word / 256) 350 | end 351 | end 352 | 353 | return enc 354 | end 355 | 356 | local function decodeInt(enc) 357 | local a = {} 358 | local encCopy = table.create(21) 359 | 360 | for i = 1, 21 do 361 | local byte = enc[i] 362 | assert(type(byte) == "number", "integer decoding failure") 363 | assert(byte >= 0 and byte <= 255, "integer decoding failure") 364 | assert(byte % 1 == 0, "integer decoding failure") 365 | encCopy[i] = byte 366 | end 367 | 368 | for i = 1, 21, 3 do 369 | local word = 0 370 | for j = 2, 0, -1 do 371 | word *= 256 372 | word += encCopy[i + j] 373 | end 374 | 375 | table.insert(a, word) 376 | end 377 | 378 | return a 379 | end 380 | 381 | local function mods(d, w) 382 | local result = d[1] % twoPower[w] 383 | 384 | if result >= twoPower[w - 1] then 385 | result -= twoPower[w] 386 | end 387 | 388 | return result 389 | end 390 | 391 | -- Represents a 168-bit number as the (2^w)-ary Non-Adjacent Form 392 | local function NAF(d, w) 393 | local t, t_len = {}, 0 394 | local newD = { table.unpack(d) } 395 | 396 | for _ = 1, 168 do 397 | if newD[1] % 2 == 1 then 398 | t_len += 1 399 | t[t_len] = mods(newD, w) 400 | newD = sub(newD, { t[#t], 0, 0, 0, 0, 0, 0 }) 401 | else 402 | t_len += 1 403 | t[t_len] = 0 404 | end 405 | 406 | newD = rShift(newD) 407 | end 408 | 409 | return t 410 | end 411 | 412 | return { 413 | isEqual = isEqual, 414 | compare = compare, 415 | add = add, 416 | sub = sub, 417 | addDouble = addDouble, 418 | mult = mult, 419 | square = square, 420 | encodeInt = encodeInt, 421 | decodeInt = decodeInt, 422 | NAF = NAF, 423 | } 424 | -------------------------------------------------------------------------------- /src/EllipticCurveCryptography/benchmark.lua: -------------------------------------------------------------------------------- 1 | local ecc = require(script.Parent) 2 | 3 | warn("Running EllipticCurveCryptography benchmark...") 4 | 5 | local function printBytes(byteTable) 6 | return table.concat(byteTable, "-") 7 | end 8 | 9 | -- Each machine generates their tokens 10 | local serverPrivate, serverPublic = ecc.keypair(ecc.random.random()) 11 | local clientPrivate, clientPublic = ecc.keypair(ecc.random.random()) 12 | 13 | -- print("\nserverPrivate:",printBytes(serverPrivate),"\nserverPublic:",printBytes(serverPublic)) 14 | -- print("\nclientPrivate:",printBytes(clientPrivate),"\nclientPublic:",printBytes(clientPublic)) 15 | 16 | -- They share their publics and exchange to shared secret 17 | local serverSecret = ecc.exchange(serverPrivate, clientPublic) 18 | local clientSecret = ecc.exchange(clientPrivate, serverPublic) 19 | 20 | --print("\nsharedSecret:", printBytes(serverSecret)) 21 | 22 | assert(tostring(serverSecret) == tostring(clientSecret), "sharedSecret must be identical to both parties") 23 | 24 | -- warn("encrypting and signing payload(s)") 25 | 26 | local N, S = 500, 100 27 | local encryptSum, decryptSum = 0, 0 28 | local data = table.create(S) 29 | for i = 1, N do 30 | for x = 1, S do 31 | data[x] = math.random(35, 120) 32 | end 33 | local payload = string.char(table.unpack(data)) 34 | 35 | local start = os.clock() 36 | local ciphertext = ecc.encrypt(payload, clientSecret) 37 | local sig = ecc.sign(clientPrivate, payload) 38 | encryptSum += os.clock() - start 39 | 40 | start = os.clock() 41 | local plaintext = ecc.decrypt(ciphertext, serverSecret) 42 | local validate = ecc.verify(clientPublic, plaintext, sig) 43 | decryptSum += os.clock() - start 44 | 45 | --print(" Bench run %d done", i) 46 | end 47 | 48 | print( 49 | string.format( 50 | " Dataset: %d payloads of %d bytes of random data.\nResults:\n Encrypt & Sign took %.2fms in total with a %.2fms avg.\n Decrypt & Verify took %.2fms in total with a %.2fms avg.", 51 | N, 52 | S, 53 | encryptSum * 1000, 54 | (encryptSum / N) * 1000, 55 | decryptSum * 1000, 56 | (decryptSum / N) * 1000 57 | ) 58 | ) 59 | 60 | return true 61 | -------------------------------------------------------------------------------- /src/EllipticCurveCryptography/chacha20.lua: -------------------------------------------------------------------------------- 1 | -- Chacha20 cipher in ComputerCraft 2 | -- By Anavrins 3 | -- For help and details, you can PM me on the CC forums 4 | -- You may use this code in your projects without asking me, as long as credit is given and this header is kept intact 5 | -- http://www.computercraft.info/forums2/index.php?/user/12870-anavrins 6 | -- http://pastebin.com/GPzf9JSa 7 | -- Last update: April 17, 2017 8 | local twoPower = require(script.Parent.twoPower) 9 | local util = require(script.Parent.util) 10 | 11 | local bxor = bit32.bxor 12 | local band = bit32.band 13 | local blshift = bit32.lshift 14 | local brshift = bit32.arshift 15 | 16 | local mod = 2 ^ 32 17 | local tau = table.pack(string.byte("expand 16-byte k", 1, -1)) 18 | local sigma = table.pack(string.byte("expand 32-byte k", 1, -1)) 19 | 20 | local function rotl(n, b) 21 | local s = n / twoPower[32 - b] 22 | local f = s % 1 23 | return (s - f) + f * mod 24 | end 25 | 26 | local function quarterRound(s, a, b, c, d) 27 | s[a] = (s[a] + s[b]) % mod 28 | s[d] = rotl(bxor(s[d], s[a]), 16) 29 | s[c] = (s[c] + s[d]) % mod 30 | s[b] = rotl(bxor(s[b], s[c]), 12) 31 | s[a] = (s[a] + s[b]) % mod 32 | s[d] = rotl(bxor(s[d], s[a]), 8) 33 | s[c] = (s[c] + s[d]) % mod 34 | s[b] = rotl(bxor(s[b], s[c]), 7) 35 | return s 36 | end 37 | 38 | local function hashBlock(state, rnd) 39 | local s = { table.unpack(state) } 40 | for i = 1, rnd do 41 | local r = i % 2 == 1 42 | s = r and quarterRound(s, 1, 5, 9, 13) or quarterRound(s, 1, 6, 11, 16) 43 | s = r and quarterRound(s, 2, 6, 10, 14) or quarterRound(s, 2, 7, 12, 13) 44 | s = r and quarterRound(s, 3, 7, 11, 15) or quarterRound(s, 3, 8, 9, 14) 45 | s = r and quarterRound(s, 4, 8, 12, 16) or quarterRound(s, 4, 5, 10, 15) 46 | end 47 | 48 | for i = 1, 16 do 49 | s[i] = (s[i] + state[i]) % mod 50 | end 51 | 52 | return s 53 | end 54 | 55 | local function LE_toInt(bs, i) 56 | return (bs[i + 1] or 0) 57 | + blshift((bs[i + 2] or 0), 8) 58 | + blshift((bs[i + 3] or 0), 16) 59 | + blshift((bs[i + 4] or 0), 24) 60 | end 61 | 62 | local function initState(key, nonce, counter) 63 | local isKey256 = #key == 32 64 | local const = isKey256 and sigma or tau 65 | local state = table.create(16) 66 | 67 | state[1] = LE_toInt(const, 0) 68 | state[2] = LE_toInt(const, 4) 69 | state[3] = LE_toInt(const, 8) 70 | state[4] = LE_toInt(const, 12) 71 | 72 | state[5] = LE_toInt(key, 0) 73 | state[6] = LE_toInt(key, 4) 74 | state[7] = LE_toInt(key, 8) 75 | state[8] = LE_toInt(key, 12) 76 | state[9] = LE_toInt(key, isKey256 and 16 or 0) 77 | state[10] = LE_toInt(key, isKey256 and 20 or 4) 78 | state[11] = LE_toInt(key, isKey256 and 24 or 8) 79 | state[12] = LE_toInt(key, isKey256 and 28 or 12) 80 | 81 | state[13] = counter 82 | state[14] = LE_toInt(nonce, 0) 83 | state[15] = LE_toInt(nonce, 4) 84 | state[16] = LE_toInt(nonce, 8) 85 | 86 | return state 87 | end 88 | 89 | local function serialize(state) 90 | local r, len_r = table.create(16), 0 91 | for i = 1, 16 do 92 | r[len_r + 1] = band(state[i], 0xFF) 93 | r[len_r + 2] = band(brshift(state[i], 8), 0xFF) 94 | r[len_r + 3] = band(brshift(state[i], 16), 0xFF) 95 | r[len_r + 4] = band(brshift(state[i], 24), 0xFF) 96 | len_r += 4 97 | end 98 | 99 | return r 100 | end 101 | 102 | local function crypt(data, key, nonce, cntr, round) 103 | assert(type(key) == "table", "ChaCha20: Invalid key format (" .. type(key) .. "), must be table") 104 | assert(type(nonce) == "table", "ChaCha20: Invalid nonce format (" .. type(nonce) .. "), must be table") 105 | assert(#key == 16 or #key == 32, "ChaCha20: Invalid key length (" .. #key .. "), must be 16 or 32") 106 | assert(#nonce == 12, "ChaCha20: Invalid nonce length (" .. #nonce .. "), must be 12") 107 | 108 | local newData = type(data) == "table" and { table.unpack(data) } or util.stringToByteArray(data) 109 | cntr = tonumber(cntr) or 1 110 | round = tonumber(round) or 20 111 | 112 | local out, out_len = {}, 0 113 | local state = initState(key, nonce, cntr) 114 | local blockAmt = math.floor(#newData / 64) 115 | for i = 0, blockAmt do 116 | local ks = serialize(hashBlock(state, round)) 117 | state[13] = (state[13] + 1) % mod 118 | 119 | local block = table.create(64) 120 | for j = 1, 64 do 121 | block[j] = newData[(i * 64) + j] 122 | end 123 | 124 | for j = 1, #block do 125 | out_len += 1 126 | out[out_len] = bxor(block[j], ks[j]) 127 | end 128 | 129 | if i % 2500 == 0 then 130 | task.wait() 131 | end 132 | end 133 | 134 | return setmetatable(out, util.byteTableMT) 135 | end 136 | 137 | return { 138 | crypt = crypt, 139 | } 140 | -------------------------------------------------------------------------------- /src/EllipticCurveCryptography/curve.lua: -------------------------------------------------------------------------------- 1 | -- Elliptic curve arithmetic 2 | 3 | ---- About the Curve Itself 4 | -- Field Size: 168 bits 5 | -- Field Modulus (p): 481 * 2^159 + 3 6 | -- Equation: x^2 + y^2 = 1 + 122 * x^2 * y^2 7 | -- Parameters: Edwards Curve with d = 122 8 | -- Curve Order (n): 351491143778082151827986174289773107581916088585564 9 | -- Cofactor (h): 4 10 | -- Generator Order (q): 87872785944520537956996543572443276895479022146391 11 | ---- About the Curve's Security 12 | -- Current best attack security: 81.777 bits (Small Subgroup + Rho) 13 | -- Rho Security: log2(0.884 * sqrt(q)) = 82.777 bits 14 | -- Transfer Security? Yes: p ~= q; k > 20 15 | -- Field Discriminant Security? Yes: 16 | -- t = 27978492958645335688000168 17 | -- s = 10 18 | -- |D| = 6231685068753619775430107799412237267322159383147 > 2^100 19 | -- Rigidity? No, not at all. 20 | -- XZ/YZ Ladder Security? No: Single coordinate ladders are insecure. 21 | -- Small Subgroup Security? No. 22 | -- Invalid Curve Security? Yes: Points are checked before every operation. 23 | -- Invalid Curve Twist Security? No: Don't use single coordinate ladders. 24 | -- Completeness? Yes: The curve is complete. 25 | -- Indistinguishability? Yes (Elligator 2), but not implemented. 26 | local util = require(script.Parent.util) 27 | local arith = require(script.Parent.arith) 28 | local modp = require(script.Parent.modp) 29 | local modq = require(script.Parent.modq) 30 | 31 | local isEqual = arith.isEqual 32 | local NAF = arith.NAF 33 | local encodeInt = arith.encodeInt 34 | local decodeInt = arith.decodeInt 35 | local multModP = modp.multModP 36 | local squareModP = modp.squareModP 37 | local addModP = modp.addModP 38 | local subModP = modp.subModP 39 | local montgomeryModP = modp.montgomeryModP 40 | local expModP = modp.expModP 41 | local inverseMontgomeryModQ = modq.inverseMontgomeryModQ 42 | 43 | local pointMT 44 | local ZERO = table.create(7, 0) 45 | local ONE = montgomeryModP({ 1, 0, 0, 0, 0, 0, 0 }) 46 | 47 | -- Curve Parameters 48 | local d = montgomeryModP({ 122, 0, 0, 0, 0, 0, 0 }) 49 | local p = { 3, 0, 0, 0, 0, 0, 15761408 } 50 | local pMinusTwoBinary = table.create(168, 0) 51 | pMinusTwoBinary[1] = 1 52 | pMinusTwoBinary[160] = 1 53 | pMinusTwoBinary[165] = 1 54 | pMinusTwoBinary[166] = 1 55 | pMinusTwoBinary[167] = 1 56 | pMinusTwoBinary[168] = 1 57 | 58 | local pMinusThreeOverFourBinary = table.create(166, 0) 59 | pMinusThreeOverFourBinary[158] = 1 60 | pMinusThreeOverFourBinary[163] = 1 61 | pMinusThreeOverFourBinary[164] = 1 62 | pMinusThreeOverFourBinary[165] = 1 63 | pMinusThreeOverFourBinary[166] = 1 64 | 65 | local G = { 66 | { 6636044, 10381432, 15741790, 2914241, 5785600, 264923, 4550291 }, 67 | { 13512827, 8449886, 5647959, 1135556, 5489843, 7177356, 8002203 }, 68 | { table.unpack(ONE) }, 69 | } 70 | 71 | local O = { 72 | table.create(7, 0), 73 | { table.unpack(ONE) }, 74 | { table.unpack(ONE) }, 75 | } 76 | 77 | -- Projective Coordinates for Edwards curves for point addition/doubling. 78 | -- Points are represented as: (X:Y:Z) where x = X/Z and y = Y/Z 79 | -- The identity element is represented by (0:1:1) 80 | -- Point operation formulas are available on the EFD: 81 | -- https://www.hyperelliptic.org/EFD/g1p/auto-edwards-projective.html 82 | local function pointDouble(P1) 83 | -- 3M + 4S 84 | local X1, Y1, Z1 = P1[1], P1[2], P1[3] 85 | 86 | local b = addModP(X1, Y1) 87 | local B = squareModP(b) 88 | local C = squareModP(X1) 89 | local D = squareModP(Y1) 90 | local E = addModP(C, D) 91 | local H = squareModP(Z1) 92 | local J = subModP(E, addModP(H, H)) 93 | local X3 = multModP(subModP(B, E), J) 94 | local Y3 = multModP(E, subModP(C, D)) 95 | local Z3 = multModP(E, J) 96 | local P3 = { X3, Y3, Z3 } 97 | 98 | return setmetatable(P3, pointMT) 99 | end 100 | 101 | local function pointAdd(P1, P2) 102 | -- 10M + 1S 103 | local X1, Y1, Z1 = P1[1], P1[2], P1[3] 104 | local X2, Y2, Z2 = P2[1], P2[2], P2[3] 105 | 106 | local A = multModP(Z1, Z2) 107 | local B = squareModP(A) 108 | local C = multModP(X1, X2) 109 | local D = multModP(Y1, Y2) 110 | local E = multModP(d, multModP(C, D)) 111 | local F = subModP(B, E) 112 | local G = addModP(B, E) 113 | local X3 = multModP(A, multModP(F, subModP(multModP(addModP(X1, Y1), addModP(X2, Y2)), addModP(C, D)))) 114 | local Y3 = multModP(A, multModP(G, subModP(D, C))) 115 | local Z3 = multModP(F, G) 116 | local P3 = { X3, Y3, Z3 } 117 | 118 | return setmetatable(P3, pointMT) 119 | end 120 | 121 | local function pointNeg(P1) 122 | local X1, Y1, Z1 = P1[1], P1[2], P1[3] 123 | 124 | local X3 = subModP(ZERO, X1) 125 | local Y3 = { table.unpack(Y1) } 126 | local Z3 = { table.unpack(Z1) } 127 | local P3 = { X3, Y3, Z3 } 128 | 129 | return setmetatable(P3, pointMT) 130 | end 131 | 132 | local function pointSub(P1, P2) 133 | return pointAdd(P1, pointNeg(P2)) 134 | end 135 | 136 | -- Converts (X:Y:Z) into (X:Y:1) = (x:y:1) 137 | local function pointScale(P1) 138 | local X1, Y1, Z1 = P1[1], P1[2], P1[3] 139 | 140 | local A = expModP(Z1, pMinusTwoBinary) 141 | local X3 = multModP(X1, A) 142 | local Y3 = multModP(Y1, A) 143 | local Z3 = { table.unpack(ONE) } 144 | local P3 = { X3, Y3, Z3 } 145 | 146 | return setmetatable(P3, pointMT) 147 | end 148 | 149 | local function pointIsEqual(P1, P2) 150 | local X1, Y1, Z1 = P1[1], P1[2], P1[3] 151 | local X2, Y2, Z2 = P2[1], P2[2], P2[3] 152 | 153 | local A1 = multModP(X1, Z2) 154 | local B1 = multModP(Y1, Z2) 155 | local A2 = multModP(X2, Z1) 156 | local B2 = multModP(Y2, Z1) 157 | 158 | return isEqual(A1, A2) and isEqual(B1, B2) 159 | end 160 | 161 | -- Checks if a projective point satisfies the curve equation 162 | local function pointIsOnCurve(P1) 163 | local X1, Y1, Z1 = P1[1], P1[2], P1[3] 164 | 165 | local X12 = squareModP(X1) 166 | local Y12 = squareModP(Y1) 167 | local Z12 = squareModP(Z1) 168 | local Z14 = squareModP(Z12) 169 | local a = addModP(X12, Y12) 170 | a = multModP(a, Z12) 171 | local b = multModP(d, multModP(X12, Y12)) 172 | b = addModP(Z14, b) 173 | 174 | return isEqual(a, b) 175 | end 176 | 177 | local function pointIsInf(P1) 178 | return isEqual(P1[1], ZERO) 179 | end 180 | 181 | -- W-ary Non-Adjacent Form (wNAF) method for scalar multiplication: 182 | -- https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#w-ary_non-adjacent_form_(wNAF)_method 183 | local function scalarMult(multiplier, P1) 184 | -- w = 5 185 | local naf = NAF(multiplier, 5) 186 | local PTable = { P1 } 187 | local P2 = pointDouble(P1) 188 | local Q = { table.create(7, 0), { table.unpack(ONE) }, { table.unpack(ONE) } } 189 | 190 | for i = 3, 31, 2 do 191 | PTable[i] = pointAdd(PTable[i - 2], P2) 192 | end 193 | 194 | for i = #naf, 1, -1 do 195 | Q = pointDouble(Q) 196 | if naf[i] > 0 then 197 | Q = pointAdd(Q, PTable[naf[i]]) 198 | elseif naf[i] < 0 then 199 | Q = pointSub(Q, PTable[-naf[i]]) 200 | end 201 | end 202 | 203 | return setmetatable(Q, pointMT) 204 | end 205 | 206 | -- Lookup table 4-ary NAF method for scalar multiplication by G. 207 | -- Precomputations for the regular NAF method are done before the multiplication. 208 | local GTable = { G } 209 | for i = 2, 168 do 210 | GTable[i] = pointDouble(GTable[i - 1]) 211 | end 212 | 213 | local function scalarMultG(multiplier) 214 | local naf = NAF(multiplier, 2) 215 | local Q = { table.create(7, 0), { table.unpack(ONE) }, { table.unpack(ONE) } } 216 | 217 | for i = 1, 168 do 218 | if naf[i] == 1 then 219 | Q = pointAdd(Q, GTable[i]) 220 | elseif naf[i] == -1 then 221 | Q = pointSub(Q, GTable[i]) 222 | end 223 | end 224 | 225 | return setmetatable(Q, pointMT) 226 | end 227 | 228 | -- Point compression and encoding. 229 | -- Compresses curve points to 22 bytes. 230 | local function pointEncode(P1) 231 | P1 = pointScale(P1) 232 | local result = {} 233 | local x, y = P1[1], P1[2] 234 | 235 | -- Encode y 236 | result = encodeInt(y) 237 | -- Encode one bit from x 238 | result[22] = x[1] % 2 239 | 240 | return setmetatable(result, util.byteTableMT) 241 | end 242 | 243 | local function pointDecode(enc) 244 | enc = type(enc) == "table" and { table.unpack(enc, 1, 22) } or { string.byte(tostring(enc), 1, 22) } 245 | -- Decode y 246 | local y = decodeInt(enc) 247 | y[7] %= p[7] 248 | -- Find {x, -x} using curve equation 249 | local y2 = squareModP(y) 250 | local u = subModP(y2, ONE) 251 | local v = subModP(multModP(d, y2), ONE) 252 | local u2 = squareModP(u) 253 | local u3 = multModP(u, u2) 254 | local u5 = multModP(u3, u2) 255 | local v3 = multModP(v, squareModP(v)) 256 | local w = multModP(u5, v3) 257 | local x = multModP(u3, multModP(v, expModP(w, pMinusThreeOverFourBinary))) 258 | -- Use enc[22] to find x from {x, -x} 259 | if x[1] % 2 ~= enc[22] then 260 | x = subModP(ZERO, x) 261 | end 262 | 263 | local P3 = { x, y, { table.unpack(ONE) } } 264 | 265 | return setmetatable(P3, pointMT) 266 | end 267 | 268 | pointMT = { 269 | __index = { 270 | isOnCurve = function(self) 271 | return pointIsOnCurve(self) 272 | end, 273 | 274 | isInf = function(self) 275 | return self:isOnCurve() and pointIsInf(self) 276 | end, 277 | 278 | encode = function(self) 279 | return pointEncode(self) 280 | end, 281 | }, 282 | 283 | __tostring = function(self) 284 | return self:encode():toHex() 285 | end, 286 | 287 | __add = function(P1, P2) 288 | assert(P1:isOnCurve(), "invalid point") 289 | assert(P2:isOnCurve(), "invalid point") 290 | 291 | return pointAdd(P1, P2) 292 | end, 293 | 294 | __sub = function(P1, P2) 295 | assert(P1:isOnCurve(), "invalid point") 296 | assert(P2:isOnCurve(), "invalid point") 297 | 298 | return pointSub(P1, P2) 299 | end, 300 | 301 | __unm = function(self) 302 | assert(self:isOnCurve(), "invalid point") 303 | 304 | return pointNeg(self) 305 | end, 306 | 307 | __eq = function(P1, P2) 308 | assert(P1:isOnCurve(), "invalid point") 309 | assert(P2:isOnCurve(), "invalid point") 310 | 311 | return pointIsEqual(P1, P2) 312 | end, 313 | 314 | __mul = function(P1, s) 315 | if type(P1) == "number" then 316 | return s * P1 317 | end 318 | 319 | if type(s) == "number" then 320 | assert(s < 16777216, "number multiplier too big") 321 | s = { s, 0, 0, 0, 0, 0, 0 } 322 | else 323 | s = inverseMontgomeryModQ(s) 324 | end 325 | 326 | if P1 == G then 327 | return scalarMultG(s) 328 | else 329 | return scalarMult(s, P1) 330 | end 331 | end, 332 | } 333 | 334 | G = setmetatable(G, pointMT) 335 | O = setmetatable(O, pointMT) 336 | 337 | return { 338 | G = G, 339 | O = O, 340 | pointDecode = pointDecode, 341 | } 342 | -------------------------------------------------------------------------------- /src/EllipticCurveCryptography/init.lua: -------------------------------------------------------------------------------- 1 | -- Elliptic Curve Cryptography in Computercraft 2 | local util = require(script.util) 3 | local sha256 = require(script.sha256) 4 | local chacha20 = require(script.chacha20) 5 | local random = require(script.random) 6 | local modq = require(script.modq) 7 | local curve = require(script.curve) 8 | 9 | local function getNonceFromEpoch() 10 | local nonce = table.create(12) 11 | local epoch = DateTime.now().UnixTimestampMillis 12 | for i = 1, 12 do 13 | nonce[i] = epoch % 256 14 | epoch = math.floor(epoch / 256) 15 | end 16 | 17 | return nonce 18 | end 19 | 20 | local function encrypt(data, key) 21 | local encKey = sha256.hmac("encKey", key) 22 | local macKey = sha256.hmac("macKey", key) 23 | local nonce = getNonceFromEpoch() 24 | 25 | local ciphertext = chacha20.crypt(data, encKey, nonce) 26 | 27 | local result = nonce 28 | for _, value in ipairs(ciphertext) do 29 | table.insert(result, value) 30 | end 31 | 32 | local mac = sha256.hmac(result, macKey) 33 | for _, value in ipairs(mac) do 34 | table.insert(result, value) 35 | end 36 | 37 | return setmetatable(result, util.byteTableMT) 38 | end 39 | 40 | local function decrypt(data, key) 41 | local actualData = type(data) == "table" and { table.unpack(data) } or { string.byte(tostring(data), 1, -1) } 42 | local encKey = sha256.hmac("encKey", key) 43 | local macKey = sha256.hmac("macKey", key) 44 | local mac = sha256.hmac({ table.unpack(actualData, 1, #actualData - 32) }, macKey) 45 | local messageMac = { table.unpack(actualData, #actualData - 31) } 46 | assert(mac:isEqual(messageMac), "invalid mac") 47 | local nonce = { table.unpack(actualData, 1, 12) } 48 | local ciphertext = { table.unpack(actualData, 13, #actualData - 32) } 49 | local result = chacha20.crypt(ciphertext, encKey, nonce) 50 | 51 | return setmetatable(result, util.byteTableMT) 52 | end 53 | 54 | local function keypair(seed) 55 | local x 56 | if seed then 57 | x = modq.hashModQ(seed) 58 | else 59 | x = modq.randomModQ() 60 | end 61 | 62 | local Y = curve.G * x 63 | 64 | local privateKey = x:encode() 65 | local publicKey = Y:encode() 66 | 67 | return privateKey, publicKey 68 | end 69 | 70 | local function exchange(privateKey, publicKey) 71 | local x = modq.decodeModQ(privateKey) 72 | local Y = curve.pointDecode(publicKey) 73 | local Z = Y * x 74 | 75 | local sharedSecret = sha256.digest(Z:encode()) 76 | 77 | return sharedSecret 78 | end 79 | 80 | local function sign(privateKey, message) 81 | local actualMessage = type(message) == "table" and string.char(table.unpack(message)) or tostring(message) 82 | local actualPrivateKey = type(privateKey) == "table" and string.char(table.unpack(privateKey)) 83 | or tostring(privateKey) 84 | 85 | local x = modq.decodeModQ(actualPrivateKey) 86 | local k = modq.randomModQ() 87 | local R = curve.G * k 88 | local e = modq.hashModQ(actualMessage .. tostring(R)) 89 | local s = k - x * e 90 | 91 | e = e:encode() 92 | s = s:encode() 93 | 94 | local result, result_len = e, #e 95 | for index, value in ipairs(s) do 96 | result[result_len + index] = value 97 | end 98 | 99 | return setmetatable(result, util.byteTableMT) 100 | end 101 | 102 | local function verify(publicKey, message, signature) 103 | local actualMessage = type(message) == "table" and string.char(table.unpack(message)) or tostring(message) 104 | local sigLen = #signature 105 | local Y = curve.pointDecode(publicKey) 106 | local e = modq.decodeModQ({ table.unpack(signature, 1, sigLen / 2) }) 107 | local s = modq.decodeModQ({ table.unpack(signature, sigLen / 2 + 1) }) 108 | local Rv = curve.G * s + Y * e 109 | local ev = modq.hashModQ(actualMessage .. tostring(Rv)) 110 | 111 | return ev == e 112 | end 113 | 114 | return { 115 | chacha20 = chacha20, 116 | sha256 = sha256, 117 | random = random, 118 | encrypt = encrypt, 119 | decrypt = decrypt, 120 | keypair = keypair, 121 | exchange = exchange, 122 | sign = sign, 123 | verify = verify, 124 | _byteMetatable = util.byteTableMT, 125 | } 126 | -------------------------------------------------------------------------------- /src/EllipticCurveCryptography/modp.lua: -------------------------------------------------------------------------------- 1 | -- Arithmetic on the finite field of integers modulo p 2 | -- Where p is the finite field modulus 3 | local arith = require(script.Parent.arith) 4 | local add = arith.add 5 | local sub = arith.sub 6 | local addDouble = arith.addDouble 7 | local mult = arith.mult 8 | local square = arith.square 9 | 10 | local p = { 3, 0, 0, 0, 0, 0, 15761408 } 11 | 12 | -- We're using the Montgomery Reduction for fast modular multiplication. 13 | -- https://en.wikipedia.org/wiki/Montgomery_modular_multiplication 14 | -- r = 2^168 15 | -- p * pInverse = -1 (mod r) 16 | -- r2 = r * r (mod p) 17 | local pInverse = { 5592405, 5592405, 5592405, 5592405, 5592405, 5592405, 14800213 } 18 | local r2 = { 13533400, 837116, 6278376, 13533388, 837116, 6278376, 7504076 } 19 | 20 | local function multByP(a) 21 | local a1, a2, a3, a4, a5, a6, a7 = a[1], a[2], a[3], a[4], a[5], a[6], a[7] 22 | 23 | local c1 = a1 * 3 24 | local c2 = a2 * 3 25 | local c3 = a3 * 3 26 | local c4 = a4 * 3 27 | local c5 = a5 * 3 28 | local c6 = a6 * 3 29 | local c7 = a1 * 15761408 30 | c7 = c7 + a7 * 3 31 | local c8 = a2 * 15761408 32 | local c9 = a3 * 15761408 33 | local c10 = a4 * 15761408 34 | local c11 = a5 * 15761408 35 | local c12 = a6 * 15761408 36 | local c13 = a7 * 15761408 37 | local c14 = 0 38 | 39 | local temp 40 | temp = c1 / 0x1000000 41 | c2 = c2 + (temp - temp % 1) 42 | c1 = c1 % 0x1000000 43 | temp = c2 / 0x1000000 44 | c3 = c3 + (temp - temp % 1) 45 | c2 = c2 % 0x1000000 46 | temp = c3 / 0x1000000 47 | c4 = c4 + (temp - temp % 1) 48 | c3 = c3 % 0x1000000 49 | temp = c4 / 0x1000000 50 | c5 = c5 + (temp - temp % 1) 51 | c4 = c4 % 0x1000000 52 | temp = c5 / 0x1000000 53 | c6 = c6 + (temp - temp % 1) 54 | c5 = c5 % 0x1000000 55 | temp = c6 / 0x1000000 56 | c7 = c7 + (temp - temp % 1) 57 | c6 = c6 % 0x1000000 58 | temp = c7 / 0x1000000 59 | c8 = c8 + (temp - temp % 1) 60 | c7 = c7 % 0x1000000 61 | temp = c8 / 0x1000000 62 | c9 = c9 + (temp - temp % 1) 63 | c8 = c8 % 0x1000000 64 | temp = c9 / 0x1000000 65 | c10 = c10 + (temp - temp % 1) 66 | c9 = c9 % 0x1000000 67 | temp = c10 / 0x1000000 68 | c11 = c11 + (temp - temp % 1) 69 | c10 = c10 % 0x1000000 70 | temp = c11 / 0x1000000 71 | c12 = c12 + (temp - temp % 1) 72 | c11 = c11 % 0x1000000 73 | temp = c12 / 0x1000000 74 | c13 = c13 + (temp - temp % 1) 75 | c12 = c12 % 0x1000000 76 | temp = c13 / 0x1000000 77 | c14 = c14 + (temp - temp % 1) 78 | c13 = c13 % 0x1000000 79 | 80 | return { c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14 } 81 | end 82 | 83 | -- Reduces a number from [0, 2p - 1] to [0, p - 1] 84 | local function reduceModP(a) 85 | -- a < p 86 | if a[7] < 15761408 or a[7] == 15761408 and a[1] < 3 then 87 | return { table.unpack(a) } 88 | end 89 | 90 | -- a > p 91 | local c1 = a[1] 92 | local c2 = a[2] 93 | local c3 = a[3] 94 | local c4 = a[4] 95 | local c5 = a[5] 96 | local c6 = a[6] 97 | local c7 = a[7] 98 | 99 | c1 = c1 - 3 100 | c7 = c7 - 15761408 101 | 102 | if c1 < 0 then 103 | c2 = c2 - 1 104 | c1 = c1 + 0x1000000 105 | end 106 | 107 | if c2 < 0 then 108 | c3 = c3 - 1 109 | c2 = c2 + 0x1000000 110 | end 111 | 112 | if c3 < 0 then 113 | c4 = c4 - 1 114 | c3 = c3 + 0x1000000 115 | end 116 | 117 | if c4 < 0 then 118 | c5 = c5 - 1 119 | c4 = c4 + 0x1000000 120 | end 121 | 122 | if c5 < 0 then 123 | c6 = c6 - 1 124 | c5 = c5 + 0x1000000 125 | end 126 | 127 | if c6 < 0 then 128 | c7 = c7 - 1 129 | c6 = c6 + 0x1000000 130 | end 131 | 132 | return { c1, c2, c3, c4, c5, c6, c7 } 133 | end 134 | 135 | local function addModP(a, b) 136 | return reduceModP(add(a, b)) 137 | end 138 | 139 | local function subModP(a, b) 140 | local result = sub(a, b) 141 | 142 | if result[7] < 0 then 143 | result = add(result, p) 144 | end 145 | 146 | return result 147 | end 148 | 149 | -- Montgomery REDC algorithn 150 | -- Reduces a number from [0, p^2 - 1] to [0, p - 1] 151 | local function REDC(T) 152 | local m = mult(T, pInverse, true) 153 | local t = { table.unpack(addDouble(T, multByP(m)), 8, 14) } 154 | 155 | return reduceModP(t) 156 | end 157 | 158 | local function multModP(a, b) 159 | -- Only works with a, b in Montgomery form 160 | return REDC(mult(a, b)) 161 | end 162 | 163 | local function squareModP(a) 164 | -- Only works with a in Montgomery form 165 | return REDC(square(a)) 166 | end 167 | 168 | local function montgomeryModP(a) 169 | return multModP(a, r2) 170 | end 171 | 172 | local function inverseMontgomeryModP(a) 173 | local newA = { table.unpack(a) } 174 | 175 | for i = 8, 14 do 176 | newA[i] = 0 177 | end 178 | 179 | return REDC(newA) 180 | end 181 | 182 | local ONE = montgomeryModP({ 1, 0, 0, 0, 0, 0, 0 }) 183 | 184 | local function expModP(base, exponentBinary) 185 | local newBase = { table.unpack(base) } 186 | local result = { table.unpack(ONE) } 187 | 188 | for i = 1, 168 do 189 | if exponentBinary[i] == 1 then 190 | result = multModP(result, newBase) 191 | end 192 | 193 | newBase = squareModP(newBase) 194 | end 195 | 196 | return result 197 | end 198 | 199 | return { 200 | addModP = addModP, 201 | subModP = subModP, 202 | multModP = multModP, 203 | squareModP = squareModP, 204 | montgomeryModP = montgomeryModP, 205 | inverseMontgomeryModP = inverseMontgomeryModP, 206 | expModP = expModP, 207 | } 208 | -------------------------------------------------------------------------------- /src/EllipticCurveCryptography/modq.lua: -------------------------------------------------------------------------------- 1 | -- Arithmetic on the Finite Field of Integers modulo q 2 | -- Where q is the generator's subgroup order. 3 | local util = require(script.Parent.util) 4 | local sha256 = require(script.Parent.sha256) 5 | local random = require(script.Parent.random) 6 | local arith = require(script.Parent.arith) 7 | 8 | local isEqual = arith.isEqual 9 | local compare = arith.compare 10 | local add = arith.add 11 | local sub = arith.sub 12 | local addDouble = arith.addDouble 13 | local mult = arith.mult 14 | local square = arith.square 15 | local encodeInt = arith.encodeInt 16 | local decodeInt = arith.decodeInt 17 | 18 | local modQMT 19 | 20 | local q = { 9622359, 6699217, 13940450, 16775734, 16777215, 16777215, 3940351 } 21 | -- this isn't an optimization, it just shortens the amount of time I have to scroll 22 | local qMinusTwoBinary = table.create(166, 1) 23 | qMinusTwoBinary[2] = 0 24 | qMinusTwoBinary[4] = 0 25 | qMinusTwoBinary[6] = 0 26 | qMinusTwoBinary[8] = 0 27 | qMinusTwoBinary[11] = 0 28 | qMinusTwoBinary[12] = 0 29 | qMinusTwoBinary[14] = 0 30 | qMinusTwoBinary[17] = 0 31 | qMinusTwoBinary[19] = 0 32 | qMinusTwoBinary[20] = 0 33 | qMinusTwoBinary[22] = 0 34 | qMinusTwoBinary[23] = 0 35 | qMinusTwoBinary[26] = 0 36 | qMinusTwoBinary[27] = 0 37 | qMinusTwoBinary[28] = 0 38 | qMinusTwoBinary[30] = 0 39 | qMinusTwoBinary[33] = 0 40 | qMinusTwoBinary[34] = 0 41 | qMinusTwoBinary[35] = 0 42 | qMinusTwoBinary[39] = 0 43 | qMinusTwoBinary[40] = 0 44 | qMinusTwoBinary[41] = 0 45 | qMinusTwoBinary[44] = 0 46 | qMinusTwoBinary[45] = 0 47 | qMinusTwoBinary[48] = 0 48 | qMinusTwoBinary[49] = 0 49 | qMinusTwoBinary[51] = 0 50 | qMinusTwoBinary[52] = 0 51 | qMinusTwoBinary[53] = 0 52 | qMinusTwoBinary[57] = 0 53 | qMinusTwoBinary[60] = 0 54 | qMinusTwoBinary[63] = 0 55 | qMinusTwoBinary[65] = 0 56 | qMinusTwoBinary[66] = 0 57 | qMinusTwoBinary[68] = 0 58 | qMinusTwoBinary[70] = 0 59 | qMinusTwoBinary[73] = 0 60 | qMinusTwoBinary[76] = 0 61 | qMinusTwoBinary[79] = 0 62 | qMinusTwoBinary[80] = 0 63 | qMinusTwoBinary[81] = 0 64 | qMinusTwoBinary[83] = 0 65 | qMinusTwoBinary[158] = 0 66 | qMinusTwoBinary[159] = 0 67 | qMinusTwoBinary[160] = 0 68 | qMinusTwoBinary[161] = 0 69 | qMinusTwoBinary[162] = 0 70 | 71 | -- We're using the Montgomery Reduction for fast modular multiplication. 72 | -- https://en.wikipedia.org/wiki/Montgomery_modular_multiplication 73 | -- r = 2^168 74 | -- q * qInverse = -1 (mod r) 75 | -- r2 = r * r (mod q) 76 | local qInverse = { 15218585, 5740955, 3271338, 9903997, 9067368, 7173545, 6988392 } 77 | local r2 = { 1336213, 11071705, 9716828, 11083885, 9188643, 1494868, 3306114 } 78 | 79 | -- Reduces a number from [0, 2q - 1] to [0, q - 1] 80 | local function reduceModQ(a) 81 | local result = { table.unpack(a) } 82 | 83 | if compare(result, q) >= 0 then 84 | result = sub(result, q) 85 | end 86 | 87 | return setmetatable(result, modQMT) 88 | end 89 | 90 | local function addModQ(a, b) 91 | return reduceModQ(add(a, b)) 92 | end 93 | 94 | local function subModQ(a, b) 95 | local result = sub(a, b) 96 | 97 | if result[7] < 0 then 98 | result = add(result, q) 99 | end 100 | 101 | return setmetatable(result, modQMT) 102 | end 103 | 104 | -- Montgomery REDC algorithn 105 | -- Reduces a number from [0, q^2 - 1] to [0, q - 1] 106 | local function REDC(T) 107 | local m = { table.unpack(mult({ table.unpack(T, 1, 7) }, qInverse, true), 1, 7) } 108 | local t = { table.unpack(addDouble(T, mult(m, q)), 8, 14) } 109 | 110 | return reduceModQ(t) 111 | end 112 | 113 | local function multModQ(a, b) 114 | -- Only works with a, b in Montgomery form 115 | return REDC(mult(a, b)) 116 | end 117 | 118 | local function squareModQ(a) 119 | -- Only works with a in Montgomery form 120 | return REDC(square(a)) 121 | end 122 | 123 | local function montgomeryModQ(a) 124 | return multModQ(a, r2) 125 | end 126 | 127 | local function inverseMontgomeryModQ(a) 128 | local newA = { table.unpack(a) } 129 | 130 | for i = 8, 14 do 131 | newA[i] = 0 132 | end 133 | 134 | return REDC(newA) 135 | end 136 | 137 | local ONE = montgomeryModQ({ 1, 0, 0, 0, 0, 0, 0 }) 138 | 139 | local function expModQ(base, exponentBinary) 140 | local newBase = { table.unpack(base) } 141 | local result = { table.unpack(ONE) } 142 | 143 | for i = 1, 168 do 144 | if exponentBinary[i] == 1 then 145 | result = multModQ(result, newBase) 146 | end 147 | 148 | newBase = squareModQ(newBase) 149 | end 150 | 151 | return result 152 | end 153 | 154 | local function intExpModQ(base, exponent) 155 | local newBase = { table.unpack(base) } 156 | local result = setmetatable({ table.unpack(ONE) }, modQMT) 157 | 158 | if exponent < 0 then 159 | newBase = expModQ(newBase, qMinusTwoBinary) 160 | exponent = -exponent 161 | end 162 | 163 | while exponent > 0 do 164 | if exponent % 2 == 1 then 165 | result = multModQ(result, newBase) 166 | end 167 | 168 | newBase = squareModQ(newBase) 169 | exponent = math.floor(exponent / 2) 170 | end 171 | 172 | return result 173 | end 174 | 175 | local function encodeModQ(a) 176 | local result = encodeInt(a) 177 | 178 | return setmetatable(result, util.byteTableMT) 179 | end 180 | 181 | local function decodeModQ(s) 182 | s = type(s) == "table" and { table.unpack(s, 1, 21) } or { string.byte(tostring(s), 1, 21) } 183 | local result = decodeInt(s) 184 | result[7] %= q[7] 185 | 186 | return setmetatable(result, modQMT) 187 | end 188 | 189 | local function randomModQ() 190 | while true do 191 | local s = { table.unpack(random.random(), 1, 21) } 192 | local result = decodeInt(s) 193 | if result[7] < q[7] then 194 | return setmetatable(result, modQMT) 195 | end 196 | end 197 | end 198 | 199 | local function hashModQ(data) 200 | return decodeModQ(sha256.digest(data)) 201 | end 202 | 203 | modQMT = { 204 | __index = { 205 | encode = function(self) 206 | return encodeModQ(self) 207 | end, 208 | }, 209 | 210 | __tostring = function(self) 211 | return self:encode():toHex() 212 | end, 213 | 214 | __add = function(self, other) 215 | if type(self) == "number" then 216 | return other + self 217 | end 218 | 219 | if type(other) == "number" then 220 | assert(other < 16777216, "number operand too big") 221 | other = montgomeryModQ({ other, 0, 0, 0, 0, 0, 0 }) 222 | end 223 | 224 | return addModQ(self, other) 225 | end, 226 | 227 | __sub = function(a, b) 228 | if type(a) == "number" then 229 | assert(a < 16777216, "number operand too big") 230 | a = montgomeryModQ({ a, 0, 0, 0, 0, 0, 0 }) 231 | end 232 | 233 | if type(b) == "number" then 234 | assert(b < 16777216, "number operand too big") 235 | b = montgomeryModQ({ b, 0, 0, 0, 0, 0, 0 }) 236 | end 237 | 238 | return subModQ(a, b) 239 | end, 240 | 241 | __unm = function(self) 242 | return subModQ(q, self) 243 | end, 244 | 245 | __eq = function(self, other) 246 | return isEqual(self, other) 247 | end, 248 | 249 | __mul = function(self, other) 250 | if type(self) == "number" then 251 | return other * self 252 | end 253 | 254 | -- EC point 255 | -- Use the point's metatable to handle multiplication 256 | if type(other) == "table" and type(other[1]) == "table" then 257 | return other * self 258 | end 259 | 260 | if type(other) == "number" then 261 | assert(other < 16777216, "number operand too big") 262 | other = montgomeryModQ({ other, 0, 0, 0, 0, 0, 0 }) 263 | end 264 | 265 | return multModQ(self, other) 266 | end, 267 | 268 | __div = function(a, b) 269 | if type(a) == "number" then 270 | assert(a < 16777216, "number operand too big") 271 | a = montgomeryModQ({ a, 0, 0, 0, 0, 0, 0 }) 272 | end 273 | 274 | if type(b) == "number" then 275 | assert(b < 16777216, "number operand too big") 276 | b = montgomeryModQ({ b, 0, 0, 0, 0, 0, 0 }) 277 | end 278 | 279 | local bInv = expModQ(b, qMinusTwoBinary) 280 | 281 | return multModQ(a, bInv) 282 | end, 283 | 284 | __pow = function(self, other) 285 | return intExpModQ(self, other) 286 | end, 287 | } 288 | 289 | return { 290 | hashModQ = hashModQ, 291 | randomModQ = randomModQ, 292 | decodeModQ = decodeModQ, 293 | inverseMontgomeryModQ = inverseMontgomeryModQ, 294 | } 295 | -------------------------------------------------------------------------------- /src/EllipticCurveCryptography/random.lua: -------------------------------------------------------------------------------- 1 | -- random.lua - Random Byte Generator 2 | local sha256 = require(script.Parent.sha256) 3 | 4 | local entropy = "" 5 | local accumulator, accumulator_len = {}, 0 6 | 7 | local function feed(data) 8 | accumulator_len += 1 9 | accumulator[accumulator_len] = tostring(data or "") 10 | end 11 | 12 | local function digest() 13 | entropy = tostring(sha256.digest(entropy .. table.concat(accumulator))) 14 | 15 | table.clear(accumulator) 16 | accumulator_len = 0 17 | end 18 | 19 | feed("init") 20 | feed(math.random(1, 2 ^ 31 - 1)) 21 | feed("|") 22 | feed(math.random(1, 2 ^ 31 - 1)) 23 | feed("|") 24 | feed(math.random(1, 2 ^ 4)) 25 | feed("|") 26 | feed(DateTime.now().UnixTimestampMillis) 27 | feed("|") 28 | for _ = 1, 10000 do 29 | feed(tostring({}):sub(-8)) 30 | end 31 | digest() 32 | feed(DateTime.now().UnixTimestampMillis) 33 | digest() 34 | 35 | local function save() 36 | feed("save") 37 | feed(DateTime.now().UnixTimestampMillis) 38 | feed({}) 39 | digest() 40 | 41 | entropy = tostring(sha256.digest(entropy)) 42 | end 43 | save() 44 | 45 | local function seed(data) 46 | feed("seed") 47 | feed(DateTime.now().UnixTimestampMillis) 48 | feed({}) 49 | feed(data) 50 | digest() 51 | save() 52 | end 53 | 54 | local function random() 55 | feed("random") 56 | feed(DateTime.now().UnixTimestampMillis) 57 | feed({}) 58 | digest() 59 | save() 60 | 61 | local result = sha256.hmac("out", entropy) 62 | entropy = tostring(sha256.digest(entropy)) 63 | 64 | return result 65 | end 66 | 67 | return { 68 | seed = seed, 69 | save = save, 70 | random = random, 71 | } 72 | -------------------------------------------------------------------------------- /src/EllipticCurveCryptography/sha256.lua: -------------------------------------------------------------------------------- 1 | -- SHA-256, HMAC and PBKDF2 functions in ComputerCraft 2 | -- By Anavrins 3 | -- For help and details, you can PM me on the CC forums 4 | -- You may use this code in your projects without asking me, as long as credit is given and this header is kept intact 5 | -- http://www.computercraft.info/forums2/index.php?/user/12870-anavrins 6 | -- http://pastebin.com/6UV4qfNF 7 | -- Last update: October 10, 2017 8 | local twoPower = require(script.Parent.twoPower) 9 | local util = require(script.Parent.util) 10 | 11 | local mod32 = 2 ^ 32 12 | local band = bit32.band 13 | local bnot = bit32.bnot 14 | local bxor = bit32.bxor 15 | local blshift = bit32.lshift 16 | 17 | local function rrotate(n, b) 18 | local s = n / twoPower[b] 19 | local f = s % 1 20 | return (s - f) + f * mod32 21 | end 22 | 23 | local function brshift(int, by) -- Thanks bit32 for bad rshift 24 | return math.floor(int / twoPower[by]) 25 | end 26 | 27 | local H = { 28 | 0x6a09e667, 29 | 0xbb67ae85, 30 | 0x3c6ef372, 31 | 0xa54ff53a, 32 | 0x510e527f, 33 | 0x9b05688c, 34 | 0x1f83d9ab, 35 | 0x5be0cd19, 36 | } 37 | 38 | local K = { 39 | 0x428a2f98, 40 | 0x71374491, 41 | 0xb5c0fbcf, 42 | 0xe9b5dba5, 43 | 0x3956c25b, 44 | 0x59f111f1, 45 | 0x923f82a4, 46 | 0xab1c5ed5, 47 | 0xd807aa98, 48 | 0x12835b01, 49 | 0x243185be, 50 | 0x550c7dc3, 51 | 0x72be5d74, 52 | 0x80deb1fe, 53 | 0x9bdc06a7, 54 | 0xc19bf174, 55 | 0xe49b69c1, 56 | 0xefbe4786, 57 | 0x0fc19dc6, 58 | 0x240ca1cc, 59 | 0x2de92c6f, 60 | 0x4a7484aa, 61 | 0x5cb0a9dc, 62 | 0x76f988da, 63 | 0x983e5152, 64 | 0xa831c66d, 65 | 0xb00327c8, 66 | 0xbf597fc7, 67 | 0xc6e00bf3, 68 | 0xd5a79147, 69 | 0x06ca6351, 70 | 0x14292967, 71 | 0x27b70a85, 72 | 0x2e1b2138, 73 | 0x4d2c6dfc, 74 | 0x53380d13, 75 | 0x650a7354, 76 | 0x766a0abb, 77 | 0x81c2c92e, 78 | 0x92722c85, 79 | 0xa2bfe8a1, 80 | 0xa81a664b, 81 | 0xc24b8b70, 82 | 0xc76c51a3, 83 | 0xd192e819, 84 | 0xd6990624, 85 | 0xf40e3585, 86 | 0x106aa070, 87 | 0x19a4c116, 88 | 0x1e376c08, 89 | 0x2748774c, 90 | 0x34b0bcb5, 91 | 0x391c0cb3, 92 | 0x4ed8aa4a, 93 | 0x5b9cca4f, 94 | 0x682e6ff3, 95 | 0x748f82ee, 96 | 0x78a5636f, 97 | 0x84c87814, 98 | 0x8cc70208, 99 | 0x90befffa, 100 | 0xa4506ceb, 101 | 0xbef9a3f7, 102 | 0xc67178f2, 103 | } 104 | 105 | local function counter(incr) 106 | local t1, t2 = 0, 0 107 | if 0xFFFFFFFF - t1 < incr then 108 | t2 = t2 + 1 109 | t1 = incr - (0xFFFFFFFF - t1) - 1 110 | else 111 | t1 = t1 + incr 112 | end 113 | 114 | return t2, t1 115 | end 116 | 117 | local function BE_toInt(bs, i) 118 | return blshift((bs[i] or 0), 24) + blshift((bs[i + 1] or 0), 16) + blshift((bs[i + 2] or 0), 8) + (bs[i + 3] or 0) 119 | end 120 | 121 | local function preprocess(data) 122 | local len = #data 123 | local data_len = #data + 1 124 | 125 | data[data_len] = 0x80 126 | while data_len % 64 ~= 56 do 127 | data_len += 1 128 | data[data_len] = 0 129 | end 130 | 131 | local blocks = math.ceil(data_len / 64) 132 | 133 | local proc = table.create(blocks) 134 | for i = 1, blocks do 135 | local block = table.create(16) 136 | proc[i] = block 137 | for j = 1, 16 do 138 | block[j] = BE_toInt(data, 1 + ((i - 1) * 64) + ((j - 1) * 4)) 139 | end 140 | end 141 | 142 | proc[blocks][15], proc[blocks][16] = counter(len * 8) 143 | return proc 144 | end 145 | 146 | local function digestblock(w, C) 147 | for j = 17, 64 do 148 | local s0 = bxor(bxor(rrotate(w[j - 15], 7), rrotate(w[j - 15], 18)), brshift(w[j - 15], 3)) 149 | local s1 = bxor(bxor(rrotate(w[j - 2], 17), rrotate(w[j - 2], 19)), brshift(w[j - 2], 10)) 150 | w[j] = (w[j - 16] + s0 + w[j - 7] + s1) % mod32 151 | end 152 | 153 | local a, b, c, d, e, f, g, h = C[1], C[2], C[3], C[4], C[5], C[6], C[7], C[8] 154 | for j = 1, 64 do 155 | local S1 = bxor(bxor(rrotate(e, 6), rrotate(e, 11)), rrotate(e, 25)) 156 | local ch = bxor(band(e, f), band(bnot(e), g)) 157 | local temp1 = (h + S1 + ch + K[j] + w[j]) % mod32 158 | local S0 = bxor(bxor(rrotate(a, 2), rrotate(a, 13)), rrotate(a, 22)) 159 | local maj = bxor(bxor(band(a, b), band(a, c)), band(b, c)) 160 | local temp2 = (S0 + maj) % mod32 161 | h, g, f, e, d, c, b, a = g, f, e, (d + temp1) % mod32, c, b, a, (temp1 + temp2) % mod32 162 | end 163 | 164 | C[1] = (C[1] + a) % mod32 165 | C[2] = (C[2] + b) % mod32 166 | C[3] = (C[3] + c) % mod32 167 | C[4] = (C[4] + d) % mod32 168 | C[5] = (C[5] + e) % mod32 169 | C[6] = (C[6] + f) % mod32 170 | C[7] = (C[7] + g) % mod32 171 | C[8] = (C[8] + h) % mod32 172 | return C 173 | end 174 | 175 | local function toBytes(t, n) 176 | local b = table.create(n * 4) 177 | for i = 1, n do 178 | b[(i - 1) * 4 + 1] = band(brshift(t[i], 24), 0xFF) 179 | b[(i - 1) * 4 + 2] = band(brshift(t[i], 16), 0xFF) 180 | b[(i - 1) * 4 + 3] = band(brshift(t[i], 8), 0xFF) 181 | b[(i - 1) * 4 + 4] = band(t[i], 0xFF) 182 | end 183 | 184 | return setmetatable(b, util.byteTableMT) 185 | end 186 | 187 | local function digest(data) 188 | data = data or "" 189 | data = type(data) == "table" and { table.unpack(data) } or util.stringToByteArray(data) 190 | 191 | data = preprocess(data) 192 | local C = { table.unpack(H) } 193 | for _, value in ipairs(data) do 194 | C = digestblock(value, C) 195 | end 196 | 197 | return toBytes(C, 8) 198 | end 199 | 200 | local function hmac(data, key) 201 | local actualData = type(data) == "table" and { table.unpack(data) } or util.stringToByteArray(data) 202 | local actualKey = type(key) == "table" and { table.unpack(key) } or util.stringToByteArray(key) 203 | 204 | local blocksize = 64 205 | 206 | actualKey = #actualKey > blocksize and digest(actualKey) or actualKey 207 | 208 | local ipad = table.create(blocksize) 209 | local opad = table.create(blocksize) 210 | 211 | for i = 1, blocksize do 212 | ipad[i] = bxor(0x36, actualKey[i] or 0) 213 | opad[i] = bxor(0x5C, actualKey[i] or 0) 214 | end 215 | 216 | for i, value in ipairs(actualData) do 217 | ipad[blocksize + i] = value 218 | end 219 | 220 | ipad = digest(ipad) 221 | local padded_key = table.create(blocksize * 2) 222 | for i = 1, blocksize do 223 | padded_key[i] = opad[i] 224 | padded_key[blocksize + i] = ipad[i] 225 | end 226 | 227 | return digest(padded_key) 228 | end 229 | 230 | local function pbkdf2(pass, salt, iter, dklen) 231 | local actualSalt = type(salt) == "table" and salt or util.stringToByteArray(salt) 232 | local hashlen = 32 233 | local actualDklen = dklen or 32 234 | local block = 1 235 | local out = {} 236 | 237 | while actualDklen > 0 do 238 | local ikey = {} 239 | local isalt = { table.unpack(actualSalt) } 240 | local isalt_len = #isalt 241 | local clen = actualDklen > hashlen and hashlen or actualDklen 242 | 243 | isalt[isalt_len + 1] = band(brshift(block, 24), 0xFF) 244 | isalt[isalt_len + 2] = band(brshift(block, 16), 0xFF) 245 | isalt[isalt_len + 3] = band(brshift(block, 8), 0xFF) 246 | isalt[isalt_len + 4] = band(block, 0xFF) 247 | 248 | isalt_len += 4 249 | 250 | for _ = 1, iter do 251 | isalt = hmac(isalt, pass) 252 | for k = 1, clen do 253 | ikey[k] = bxor(isalt[k], ikey[k] or 0) 254 | end 255 | -- if j % 200 == 0 then 256 | -- coroutine.yield("PBKDF2") 257 | -- end 258 | end 259 | 260 | actualDklen = actualDklen - clen 261 | block = block + 1 262 | for k = 1, clen do 263 | out[k] = ikey[k] 264 | end 265 | end 266 | 267 | return setmetatable(out, util.byteTableMT) 268 | end 269 | 270 | return { 271 | digest = digest, 272 | hmac = hmac, 273 | pbkdf2 = pbkdf2, 274 | } 275 | -------------------------------------------------------------------------------- /src/EllipticCurveCryptography/testing.lua: -------------------------------------------------------------------------------- 1 | local ecc = require(script.Parent) 2 | 3 | warn("Running EllipticCurveCryptography tests...") 4 | 5 | local function printBytes(byteTable) 6 | return table.concat(byteTable, "-") 7 | end 8 | 9 | -- Each machine generates their tokens 10 | local serverPrivate, serverPublic = ecc.keypair(ecc.random.random()) 11 | local clientPrivate, clientPublic = ecc.keypair(ecc.random.random()) 12 | 13 | -- print("\nserverPrivate:",printBytes(serverPrivate),"\nserverPublic:",printBytes(serverPublic)) 14 | -- print("\nclientPrivate:",printBytes(clientPrivate),"\nclientPublic:",printBytes(clientPublic)) 15 | 16 | -- They share their publics and exchange to shared secret 17 | local serverSecret = ecc.exchange(serverPrivate, clientPublic) 18 | local clientSecret = ecc.exchange(clientPrivate, serverPublic) 19 | 20 | --print("\nsharedSecret:", printBytes(serverSecret)) 21 | 22 | assert(tostring(serverSecret) == tostring(clientSecret), "sharedSecret must be identical to both parties") 23 | 24 | local data = table.create(50) 25 | for i = 1, 100 do 26 | for x = 1, 50 do 27 | data[x] = math.random(35, 120) 28 | end 29 | local payload = string.char(table.unpack(data)) 30 | 31 | -- Client encrypts and signs their password payload 32 | local ciphertext = ecc.encrypt(payload, clientSecret) 33 | local sig = ecc.sign(clientPrivate, payload) 34 | 35 | -- print("\nencryptedPayload:",printBytes(ciphertext),"\nsignature:",printBytes(sig)) 36 | 37 | -- warn("decrypting and verifying payload") 38 | -- Server recieves and validates 39 | local plaintext = ecc.decrypt(ciphertext, serverSecret) 40 | local validate = ecc.verify(clientPublic, plaintext, sig) 41 | 42 | -- print("\ndecryptedPayload:",plaintext,"\nverified:",validate) 43 | 44 | assert(payload ~= tostring(ciphertext), "Encrypted payload must be different from plaintext") 45 | assert(payload == tostring(plaintext), "Decrypted data must equal the original payload") 46 | assert(validate, "Signature must verify decrypted data") 47 | 48 | --print(" Test run %d passed", i) 49 | end 50 | 51 | print(" EllipticCurveCryptography tests passed") 52 | 53 | return true 54 | -------------------------------------------------------------------------------- /src/EllipticCurveCryptography/twoPower.lua: -------------------------------------------------------------------------------- 1 | local twoPower = setmetatable({}, { 2 | __index = function(self, index) 3 | local value = 2 ^ index 4 | self[index] = value 5 | return value 6 | end, 7 | }) 8 | 9 | -- precache 10 | for index = -512, 512 do 11 | local _ = twoPower[index] 12 | end 13 | 14 | return twoPower 15 | -------------------------------------------------------------------------------- /src/EllipticCurveCryptography/util.lua: -------------------------------------------------------------------------------- 1 | local util = {} 2 | 3 | util.byteTableMT = { 4 | __tostring = function(a) 5 | return string.char(table.unpack(a)) 6 | end, 7 | 8 | __index = { 9 | toHex = function(self) 10 | return string.format(string.rep("%02x", #self), table.unpack(self)) 11 | end, 12 | 13 | isEqual = function(self, t) 14 | if type(t) ~= "table" then 15 | return false 16 | end 17 | 18 | if #self ~= #t then 19 | return false 20 | end 21 | 22 | local ret = 0 23 | for index, value in ipairs(self) do 24 | ret = bit32.bor(ret, bit32.bxor(value, t[index])) 25 | end 26 | 27 | return ret == 0 28 | end, 29 | }, 30 | } 31 | 32 | function util.stringToByteArray(str) 33 | if type(str) ~= "string" then 34 | return {} 35 | end 36 | 37 | local length = #str 38 | if length < 7000 then 39 | return table.pack(string.byte(str, 1, -1)) 40 | end 41 | 42 | local arr = table.create(length) 43 | for i = 1, length do 44 | arr[i] = string.byte(str, i) 45 | end 46 | 47 | return arr 48 | end 49 | 50 | return util 51 | -------------------------------------------------------------------------------- /src/client.lua: -------------------------------------------------------------------------------- 1 | local HttpService = game:GetService("HttpService") 2 | 3 | local ECC = require(script.Parent.EllipticCurveCryptography) 4 | 5 | local HandshakeRemote = script.Parent:WaitForChild("Handshake") 6 | 7 | local clientPrivate, clientPublic = ECC.keypair(ECC.random.random()) 8 | local serverPublic = HandshakeRemote:InvokeServer(clientPublic) 9 | local sharedSecret = ECC.exchange(clientPrivate, serverPublic) 10 | 11 | return function(Remote) 12 | local Wrapper = setmetatable({}, { __index = Remote }) 13 | 14 | -- Event 15 | 16 | function Wrapper:SendToServer(...) 17 | local args = table.pack(...) 18 | local data = HttpService:JSONEncode(args) 19 | 20 | local encryptedData = ECC.encrypt(data, sharedSecret) 21 | local signature = ECC.sign(clientPrivate, data) 22 | 23 | return Remote:SendToServer(encryptedData, signature) 24 | end 25 | 26 | function Wrapper:Connect(callback) 27 | Remote:Connect(function(encryptedData, signature) 28 | -- Metatables get lost in transit 29 | setmetatable(encryptedData, ECC._byteMetatable) 30 | setmetatable(signature, ECC._byteMetatable) 31 | 32 | local data = ECC.decrypt(encryptedData, sharedSecret) 33 | local verified = ECC.verify(serverPublic, data, signature) 34 | 35 | if not verified then 36 | warn("Could not verify signature", Remote.instance.Name) 37 | return 38 | end 39 | 40 | local args = HttpService:JSONDecode(tostring(data)) 41 | callback(table.unpack(args)) 42 | end) 43 | end 44 | 45 | -- AsyncFunction 46 | 47 | function Wrapper:CallServerAsync(...) 48 | local args = table.pack(...) 49 | local data = HttpService:JSONEncode(args) 50 | 51 | local encryptedData = ECC.encrypt(data, sharedSecret) 52 | local signature = ECC.sign(clientPrivate, data) 53 | 54 | return Remote:CallServerAsync(encryptedData, signature) 55 | end 56 | 57 | function Wrapper:SetCallback(callback) 58 | Remote:SetCallback(function(encryptedData, signature) 59 | -- Metatables get lost in transit 60 | setmetatable(encryptedData, ECC._byteMetatable) 61 | setmetatable(signature, ECC._byteMetatable) 62 | 63 | local data = ECC.decrypt(encryptedData, sharedSecret) 64 | local verified = ECC.verify(serverPublic, data, signature) 65 | 66 | if not verified then 67 | warn("Could not verify signature", Remote.instance.Name) 68 | return 69 | end 70 | 71 | local args = HttpService:JSONDecode(tostring(data)) 72 | local success, response = pcall(callback, table.unpack(args)) 73 | 74 | if not success then 75 | warn("Error in callback", Remote.instance.Name, response) 76 | return 77 | end 78 | 79 | return response 80 | end) 81 | end 82 | 83 | return Wrapper 84 | end 85 | -------------------------------------------------------------------------------- /src/init.lua: -------------------------------------------------------------------------------- 1 | if game:GetService("RunService"):IsServer() then 2 | -- Server side 3 | return require(script.server) 4 | else 5 | -- Client side 6 | return require(script.client) 7 | end 8 | -------------------------------------------------------------------------------- /src/server.lua: -------------------------------------------------------------------------------- 1 | local Players = game:GetService("Players") 2 | local HttpService = game:GetService("HttpService") 3 | 4 | local ECC = require(script.Parent.EllipticCurveCryptography) 5 | 6 | local PlayerData = {} 7 | 8 | local HandshakeRemote = Instance.new("RemoteFunction") 9 | HandshakeRemote.Name = "Handshake" 10 | HandshakeRemote.Parent = script.Parent 11 | 12 | function HandshakeRemote.OnServerInvoke(Player, clientPublic) 13 | local serverPrivate, serverPublic = ECC.keypair(ECC.random.random()) 14 | 15 | PlayerData[Player] = { 16 | clientPublic = clientPublic, 17 | serverPublic = serverPublic, 18 | serverPrivate = serverPrivate, 19 | sharedSecret = ECC.exchange(serverPrivate, clientPublic), 20 | } 21 | 22 | return serverPublic 23 | end 24 | 25 | Players.PlayerRemoving:Connect(function(Player) 26 | PlayerData[Player] = nil 27 | end) 28 | 29 | local function getPlayerData(Player, shouldYield) 30 | local playerData = PlayerData[Player] 31 | if shouldYield and playerData == nil then 32 | -- Perhaps they're in middle of the handshake? 33 | -- Poll for half a second 34 | for _=1, 15 do 35 | task.wait(1/30) 36 | playerData = PlayerData[Player] 37 | if playerData then 38 | break 39 | end 40 | end 41 | end 42 | 43 | return playerData 44 | end 45 | 46 | return function(Remote) 47 | local Wrapper = setmetatable({}, { __index = Remote }) 48 | 49 | -- Event 50 | 51 | function Wrapper:Connect(callback) 52 | Remote:Connect(function(Player, encryptedData, signature) 53 | local playerData = getPlayerData(Player, false) -- No yield since they can't have sent valid data without a handshake first 54 | if not playerData then 55 | return 56 | end 57 | 58 | local secret = playerData.sharedSecret 59 | local clientPublic = playerData.clientPublic 60 | 61 | -- Metatables get lost in transit 62 | setmetatable(encryptedData, ECC._byteMetatable) 63 | setmetatable(signature, ECC._byteMetatable) 64 | 65 | local data = ECC.decrypt(encryptedData, secret) 66 | local verified = ECC.verify(clientPublic, data, signature) 67 | 68 | if not verified then 69 | warn("Could not verify signature", Remote.instance.Name) 70 | return 71 | end 72 | 73 | local args = HttpService:JSONDecode(tostring(data)) 74 | callback(Player, table.unpack(args)) 75 | end) 76 | end 77 | 78 | function Wrapper:SendToPlayer(Player, ...) 79 | local playerData = getPlayerData(Player, true) -- Yield in case we're sending before they're ready 80 | if not playerData then 81 | return 82 | end 83 | local secret = playerData.sharedSecret 84 | local private = playerData.serverPrivate 85 | 86 | local args = table.pack(...) 87 | local data = HttpService:JSONEncode(args) 88 | 89 | local encryptedData = ECC.encrypt(data, secret) 90 | local signature = ECC.sign(private, data) 91 | 92 | Remote:SendToPlayer(Player, encryptedData, signature) 93 | end 94 | 95 | function Wrapper:SendToPlayers(Allowlist, ...) 96 | for _, Player in ipairs(Players:GetPlayers()) do 97 | if not table.find(Allowlist, Player) then 98 | continue 99 | end 100 | 101 | task.spawn(Wrapper.SendToPlayer, Wrapper, Player, ...) 102 | end 103 | end 104 | 105 | function Wrapper:SendToAllPlayers(...) 106 | for _, Player in ipairs(Players:GetPlayers()) do 107 | task.spawn(Wrapper.SendToPlayer, Wrapper, Player, ...) 108 | end 109 | end 110 | 111 | function Wrapper:SendToAllPlayersExcept(Ignorelist, ...) 112 | for _, Player in ipairs(Players:GetPlayers()) do 113 | if Ignorelist == Player then 114 | continue 115 | end 116 | if type(Ignorelist) == "table" and table.find(Ignorelist, Player) then 117 | continue 118 | end 119 | 120 | task.spawn(Wrapper.SendToPlayer, Wrapper, Player, ...) 121 | end 122 | end 123 | 124 | -- AsyncFunction 125 | 126 | function Wrapper:CallPlayerAsync(Player, ...) 127 | local playerData = getPlayerData(Player, true) -- Yield in case we're calling before they're ready 128 | if not playerData then 129 | return 130 | end 131 | local secret = playerData.sharedSecret 132 | local private = playerData.serverPrivate 133 | 134 | local args = table.pack(...) 135 | local data = HttpService:JSONEncode(args) 136 | 137 | local encryptedData = ECC.encrypt(data, secret) 138 | local signature = ECC.sign(private, data) 139 | 140 | return Remote:CallPlayerAsync(Player, encryptedData, signature) 141 | end 142 | 143 | function Wrapper:SetCallback(callback) 144 | Remote:SetCallback(function(Player, encryptedData, signature) 145 | local playerData = getPlayerData(Player, false) -- No yield since they can't have sent valid data without a handshake first 146 | if not playerData then 147 | return 148 | end 149 | local secret = playerData.sharedSecret 150 | local clientPublic = playerData.clientPublic 151 | 152 | -- Metatables get lost in transit 153 | setmetatable(encryptedData, ECC._byteMetatable) 154 | setmetatable(signature, ECC._byteMetatable) 155 | 156 | local data = ECC.decrypt(encryptedData, secret) 157 | local verified = ECC.verify(clientPublic, data, signature) 158 | 159 | if not verified then 160 | warn("Could not verify signature", Remote.instance.Name) 161 | return 162 | end 163 | 164 | local args = HttpService:JSONDecode(tostring(data)) 165 | local success, response = pcall(callback, Player, table.unpack(args)) 166 | 167 | if not success then 168 | warn("Error in callback", Remote.instance.Name, response) 169 | return 170 | end 171 | 172 | return response 173 | end) 174 | end 175 | 176 | return Wrapper 177 | end 178 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | # column_width = 248347834343 2 | column_width = 120 3 | quote_style = "ForceDouble" -------------------------------------------------------------------------------- /tests.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tests", 3 | "tree": { 4 | "$className": "DataModel", 5 | 6 | "Workspace": { 7 | "$className": "Workspace", 8 | "$path": "Tests", 9 | 10 | "DevPackages": { 11 | "$path": "DevPackages" 12 | }, 13 | 14 | "ECC": { 15 | "$path": "src/EllipticCurveCryptography" 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /wally.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Wally. 2 | # It is not intended for manual editing. 3 | registry = "test" 4 | 5 | [[package]] 6 | name = "boatbomber/boattest" 7 | version = "0.1.1" 8 | dependencies = [] 9 | 10 | [[package]] 11 | name = "boatbomber/encryptednet" 12 | version = "1.0.4" 13 | dependencies = [] 14 | -------------------------------------------------------------------------------- /wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "boatbomber/encryptednet" 3 | description = "Authenticated encryption of Roblox networking with ECDH key exchanges and ChaCha20 ciphering." 4 | version = "1.0.4" 5 | license = "MIT" 6 | authors = ["boatbomber (https://boatbomber.com)"] 7 | registry = "https://github.com/upliftgames/wally-index" 8 | realm = "shared" 9 | include = ["src", "src/**", "wally.toml", "wally.lock", "default.project.json"] 10 | exclude = ["**"] 11 | 12 | [dev-dependencies] 13 | BoatTEST = "boatbomber/boattest@^0.1.1" 14 | --------------------------------------------------------------------------------