diff --git a/code/__DEFINES/rust.dm b/code/__DEFINES/rust.dm
index ce5382642f3..6ea0d162ffd 100644
--- a/code/__DEFINES/rust.dm
+++ b/code/__DEFINES/rust.dm
@@ -37,14 +37,18 @@
else
// First check if it's built in the usual place.
if(fexists("./rust/target/i686-pc-windows-msvc/debug/rustlibs.dll"))
+ log_world("\[Rustlibs] Loading debug build")
return __rustlib = "./rust/target/i686-pc-windows-msvc/debug/rustlibs.dll"
if(fexists("./rust/target/i686-pc-windows-msvc/release/rustlibs.dll"))
+ log_world("\[Rustlibs] Loading release build")
return __rustlib = "./rust/target/i686-pc-windows-msvc/release/rustlibs.dll"
// Then check in the current directory.
if(fexists("./rustlibs[RUSTLIBS_SUFFIX].dll"))
+ log_world("\[Rustlibs] Loading repository build A")
return __rustlib = "./rustlibs[RUSTLIBS_SUFFIX].dll"
// And elsewhere.
+ log_world("\[Rustlibs] Loading repository build B")
var/assignment_confirmed = (__rustlib = "rustlibs[RUSTLIBS_SUFFIX].dll")
// This being spanned over multiple lines is kinda scuffed, but its needed because of https://www.byond.com/forum/post/2072419
return assignment_confirmed
@@ -133,6 +137,23 @@
/proc/mapmanip_read_dmm(mapname)
return RUSTLIB_CALL(mapmanip_read_dmm_file, mapname)
+// MARK: MFA
+
+/proc/rustlibs_mfa_generate_secret()
+ return RUSTLIB_CALL(mfa_generate_secret)
+
+/proc/rustlibs_mfa_generate_qr(secret, ckey)
+ return RUSTLIB_CALL(mfa_generate_qr, secret, ckey)
+
+/proc/rustlibs_mfa_verify_code(secret, code)
+ return RUSTLIB_CALL(mfa_verify_code, secret, code)
+
+
+// MARK: Misc
+
+/proc/rustlibs_generate_uuid()
+ return RUSTLIB_CALL(misc_new_uuid)
+
// MARK: TOML
/proc/rustlibs_read_toml_file(path)
var/list/output = json_decode(RUSTLIB_CALL(toml_file_to_json, path) || "null")
diff --git a/code/__HELPERS/api.dm b/code/__HELPERS/api.dm
deleted file mode 100644
index 5941dad585a..00000000000
--- a/code/__HELPERS/api.dm
+++ /dev/null
@@ -1,40 +0,0 @@
-// This file contains procs for interacting with the internal API
-
-/**
- * Internal API Caller
- *
- * Makes calls to the internal Paradise API and returns a [/datum/http_response].
- *
- * Arguments:
- * * method - The relevant HTTP method to use
- * * path - The path of the API call. DO NOT USE A LEADING SLASH
- * * body - The request body, if applicable
- */
-/proc/MakeAPICall(method, path, body)
- if(!method || !path)
- // Needs valid params
- return null
- if(!GLOB.configuration.system.api_host || !GLOB.configuration.system.api_key)
- // Needs these set in config
- return null
-
- if(IsAdminAdvancedProcCall())
- // Admins shouldnt fuck with this
- to_chat(usr, SPAN_BOLDANNOUNCEOOC("API interaction blocked: Advanced ProcCall detected."))
- message_admins("[key_name(usr)] attempted to interact with the internal API via advanced proc-call")
- log_admin("[key_name(usr)] attempted to interact with the internal API via advanced proc-call")
- return
-
- var/datum/http_request/req = new()
- var/target_url = "[GLOB.configuration.system.api_host]/[path]"
- // You may be asking, "Hey AA, why is the above a var instead of just using it directly?"
- // Ill tell you why. This lets you breakpoint it in the debugger to see if the URL is
- // what you wanted or not, given how slashes and stuff can break it.
- req.prepare(method, target_url, body, list("AuthKey" = GLOB.configuration.system.api_key))
- req.begin_async()
- // Check if we are complete
- UNTIL(req.is_complete())
- var/datum/http_response/res = req.into_response()
-
- return res
-
diff --git a/code/controllers/configuration/sections/system_configuration.dm b/code/controllers/configuration/sections/system_configuration.dm
index eac494de3e0..33769bd75bc 100644
--- a/code/controllers/configuration/sections/system_configuration.dm
+++ b/code/controllers/configuration/sections/system_configuration.dm
@@ -10,10 +10,6 @@
var/is_production = FALSE
/// If above is true, you can run a shell command
var/shutdown_shell_command = null
- /// Internal API host
- var/api_host = null
- /// Internal API key
- var/api_key = null
/// Github API token
var/github_api_token = null
/// List of IP addresses which bypass world topic rate limiting
@@ -47,8 +43,6 @@
CONFIG_LOAD_STR(topic_key, data["communications_password"])
CONFIG_LOAD_STR(shutdown_shell_command, data["shutdown_shell_command"])
- CONFIG_LOAD_STR(api_host, data["api_host"])
- CONFIG_LOAD_STR(api_key, data["api_key"])
CONFIG_LOAD_STR(github_api_token, data["github_api_token"])
CONFIG_LOAD_LIST(topic_ip_ratelimit_bypass, data["topic_ip_ratelimit_bypass"])
diff --git a/code/modules/admin/IsBanned.dm b/code/modules/admin/IsBanned.dm
index 61fd2674915..8a23e2668da 100644
--- a/code/modules/admin/IsBanned.dm
+++ b/code/modules/admin/IsBanned.dm
@@ -77,7 +77,7 @@
// If 2FA is enabled, makes sure they were authed within the last minute
- if(check_2fa && GLOB.configuration.system.api_host)
+ if(check_2fa)
// First see if they exist at all
var/datum/db_query/check_query = SSdbcore.NewQuery("SELECT 2fa_status, ip FROM player WHERE ckey=:ckey", list("ckey" = ckey(key)))
diff --git a/code/modules/admin/verbs/debug_verbs.dm b/code/modules/admin/verbs/debug_verbs.dm
index 5b4a023e49f..3e55fcbebd5 100644
--- a/code/modules/admin/verbs/debug_verbs.dm
+++ b/code/modules/admin/verbs/debug_verbs.dm
@@ -38,10 +38,10 @@ USER_VERB(advanced_proccall, R_PROCCALL, "Advanced ProcCall", "Advanced ProcCall
if(!procname) return
// absolutely not
- if(findtextEx(trim(lowertext(procname)), "rustg"))
- message_admins(SPAN_USERDANGER("[key_name_admin(client)] attempted to proc call rust-g procs. Inform the host at once."))
- log_admin("[key_name(client)] attempted to proc call rust-g procs. Inform the host at once.")
- GLOB.discord_manager.send2discord_simple(DISCORD_WEBHOOK_ADMIN, "[key_name(client)] attempted to proc call rustg things. Inform the host at once.")
+ if(findtextEx(trim(lowertext(procname)), "rustlibs"))
+ message_admins(SPAN_USERDANGER("[key_name_admin(client)] attempted to proc call rustlibs procs. Inform the host at once."))
+ log_admin("[key_name(client)] attempted to proc call rustlibs procs. Inform the host at once.")
+ GLOB.discord_manager.send2discord_simple(DISCORD_WEBHOOK_ADMIN, "[key_name(client)] attempted to proc call rustlibs procs. Inform the host at once.")
return
if(targetselected && !hascall(target,procname))
diff --git a/code/modules/client/2fa.dm b/code/modules/client/2fa.dm
index 52c83eff38a..4e9f0677b44 100644
--- a/code/modules/client/2fa.dm
+++ b/code/modules/client/2fa.dm
@@ -1,61 +1,47 @@
// This is in its own file as it has so much stuff to contend with
/client/proc/edit_2fa()
- if(!GLOB.configuration.system.api_host)
- alert(usr, "This server does not have 2FA enabled.")
- return
// Client does not have 2FA enabled. Set it up.
if(prefs._2fa_status == _2FA_DISABLED)
- // Get us an auth token
- var/datum/http_response/qrcr = MakeAPICall(RUSTLIBS_HTTP_METHOD_GET, "2fa/generate_qr?ckey=[ckey]")
- // If this fails, shits gone bad
- if(qrcr.errored)
- alert(usr, "Something has gone VERY wrong ingame. Please inform the server host.\nError details: [qrcr.error]")
- return
+ // Step 1 - Generate a secret
+ var/mfa_secret = rustlibs_mfa_generate_secret()
- if(qrcr.status_code != 200)
- alert(usr, "2FA QR code generation returned a non-200 code. Please inform the server host.\nError code: [qrcr.status_code]\nError details: [qrcr.body]")
- return
+ // Step 2 - Generate QR code
+ var/mfa_qr_code = rustlibs_mfa_generate_qr(mfa_secret, ckey)
- var/list/data = json_decode(qrcr.body)
- var/qr_img_src = data["qr_image"]
- var/datum/browser/B = new(usr, "2fa_qrc", "2FA QR Code", 600, 400)
+ var/datum/browser/B = new(usr, "2fa_qrc", "2FA QR Code", 600, 560)
var/title_text = "
Below is a QR code to scan inside your authenticator app to generate 2FA codes. Please verify it before closing this window. (Behind this window is a text box)
"
- var/img_data = "
"
+ var/img_data = "
"
B.set_content("[title_text][img_data]")
B.open(FALSE)
var/entered_code = input(usr, "Please enter a code from your auth app. Failure to enter the code correctly will abort 2FA setup.", "2FA Validation")
if(!entered_code)
- // Cleanup so they can start again
- var/datum/db_query/dbq = SSdbcore.NewQuery("DELETE FROM 2fa_secrets WHERE ckey=:ckey", list("ckey" = ckey))
- dbq.warn_execute()
alert(usr, "2FA Setup aborted!")
B.close()
return
- var/datum/http_response/vr = MakeAPICall(RUSTLIBS_HTTP_METHOD_GET, "2fa/validate_code?ckey=[ckey]&code=[entered_code]")
- // If this fails, shits gone bad
- if(vr.errored)
- alert(usr, "Something has gone VERY wrong ingame. Please inform the server host.\nError details: [vr.error]")
+ // See if the code entered is corrected
+ var/mfa_success = rustlibs_mfa_verify_code(mfa_secret, entered_code)
+
+ if(!mfa_success)
+ alert(usr, "Incorrect MFA code entered - check your phone date and time settings.")
B.close()
return
- if(vr.status_code != 200)
- // Cleanup so they can start again
- var/datum/db_query/dbq = SSdbcore.NewQuery("DELETE FROM 2fa_secrets WHERE ckey=:ckey", list("ckey" = ckey))
- dbq.warn_execute()
-
- // See if its unauthorised. I used 400 for that dont at me
- if(vr.status_code == 400)
- alert(usr, "Invalid code entered. 2FA Setup aborted!")
- B.close()
- else
- alert(usr, "2FA validation returned a non-200 code. Please inform the server host.\nError code: [vr.status_code]\nError details: [vr.body]")
- B.close()
- return
-
// If we are here, they authed successfully
B.close()
+
+ // Do our DB update
+ var/datum/db_query/insert_qry = SSdbcore.NewQuery("INSERT INTO 2fa_secrets (ckey, secret) VALUES (:ckey, :secret)", list(
+ "ckey" = ckey,
+ "secret" = mfa_secret
+ ))
+
+ if(!insert_qry.warn_execute())
+ qdel(insert_qry)
+ alert(usr, "MFA failed to save to the DB - please inform the server host.")
+ return
+
// Default to IP change only
prefs._2fa_status = _2FA_ENABLED_IP
prefs.save_preferences(src)
@@ -93,18 +79,31 @@
alert(usr, "2FA deactivation aborted!")
return
- var/datum/http_response/vr = MakeAPICall(RUSTLIBS_HTTP_METHOD_GET, "2fa/validate_code?ckey=[ckey]&code=[entered_code]")
- // If this fails, shits gone bad
- if(vr.errored)
- alert(usr, "Something has gone VERY wrong ingame. Please inform the server host.\nError details: [vr.error]")
+ // Get their secret
+ var/datum/db_query/load_qry = SSdbcore.NewQuery("SELECT secret FROM 2fa_secrets WHERE ckey=:ckey", list(
+ "ckey" = ckey,
+ ))
+
+ if(!load_qry.warn_execute())
+ qdel(load_qry)
+ alert(usr, "Failed to load your existing MFA token - please inform the server host.")
return
- if(vr.status_code != 200)
- // See if its unauthorised. I used 400 for that dont at me
- if(vr.status_code == 400)
- alert(usr, "Invalid code entered. 2FA deactivation aborted!")
- else
- alert(usr, "2FA validation returned non-200 code. Please inform the server host.\nError code: [vr.status_code]\nError details: [vr.body]")
+ var/db_secret = null
+
+ if(load_qry.NextRow())
+ db_secret = load_qry.item[1]
+
+ qdel(load_qry)
+
+ if(!db_secret)
+ alert(usr, "Failed to load your existing MFA token - please inform the server host.")
+ return
+
+ var/mfa_result = rustlibs_mfa_verify_code(db_secret, entered_code)
+
+ if(!mfa_result)
+ alert(usr, "Invalid code entered. 2FA deactivation aborted. If you have lost your authenticator, please inform the server host.")
return
// If we are here, they authed properly
diff --git a/config/example/config.toml b/config/example/config.toml
index eefb6d305b5..5ae84466b88 100644
--- a/config/example/config.toml
+++ b/config/example/config.toml
@@ -799,10 +799,6 @@ active_lava_ruins = [
shutdown_on_reboot = false
# If true to above, an optional shell command can be ran. Defaults to killing DD on windows
#shutdown_shell_command = "taskkill /im dreamdaemon.exe /f"
-# URL for the internal API host. See https://github.com/ParadiseSS13/ParadiseInternalAPI Do not use https:// or a trailing slash. Comment out to disable
-#api_host = "http://127.0.0.1:8080"
-# Access key for the internal API.
-#api_key = "your_secret_here"
# Github API token for in game bug reporting
#github_api_token = "your_secret_here"
# List of IP addresses to be ignored by the world/Topic rate limiting. Useful if you have other services
diff --git a/paradise.dme b/paradise.dme
index dee7109402c..712343a8999 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -200,7 +200,6 @@
#include "code\__HELPERS\_logging.dm"
#include "code\__HELPERS\_string_lists.dm"
#include "code\__HELPERS\AnimationLibrary.dm"
-#include "code\__HELPERS\api.dm"
#include "code\__HELPERS\atom_helpers.dm"
#include "code\__HELPERS\basic_ai.dm"
#include "code\__HELPERS\bitflag_lists.dm"
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index 957592c0f20..4b617fc4ffd 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -228,6 +228,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+[[package]]
+name = "base32"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076"
+
[[package]]
name = "base64"
version = "0.22.1"
@@ -386,6 +392,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
[[package]]
name = "bytes"
version = "1.11.1"
@@ -489,6 +501,12 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "constant_time_eq"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
+
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -615,7 +633,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a92e2d3660e5513454071018433d22bbdfc8a79b9bcf1b6399aa72e96b8586"
dependencies = [
"lerp",
- "rand",
+ "rand 0.8.6",
"rand_pcg",
"rand_seeder",
"rayon",
@@ -677,6 +695,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
+ "subtle",
]
[[package]]
@@ -714,7 +733,7 @@ dependencies = [
"inflate",
"lodepng",
"ndarray",
- "rand",
+ "rand 0.8.6",
]
[[package]]
@@ -1030,6 +1049,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
[[package]]
name = "iana-time-zone"
version = "0.1.63"
@@ -1167,6 +1195,19 @@ dependencies = [
"icu_properties",
]
+[[package]]
+name = "image"
+version = "0.25.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "moxcms",
+ "num-traits",
+ "png 0.18.1",
+]
+
[[package]]
name = "indenter"
version = "0.3.3"
@@ -1438,6 +1479,16 @@ dependencies = [
"simd-adler32",
]
+[[package]]
+name = "moxcms"
+version = "0.7.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
+dependencies = [
+ "num-traits",
+ "pxfm",
+]
+
[[package]]
name = "mysql"
version = "26.0.0"
@@ -1500,7 +1551,7 @@ dependencies = [
"mysql-common-derive",
"num-bigint",
"num-traits",
- "rand",
+ "rand 0.8.6",
"regex",
"saturating",
"serde",
@@ -1762,7 +1813,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
- "rand",
+ "rand 0.8.6",
]
[[package]]
@@ -1843,6 +1894,19 @@ dependencies = [
"miniz_oxide",
]
+[[package]]
+name = "png"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
+dependencies = [
+ "bitflags 2.9.1",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
[[package]]
name = "polling"
version = "3.9.0"
@@ -1966,6 +2030,29 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "pxfm"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
+
+[[package]]
+name = "qrcodegen"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142"
+
+[[package]]
+name = "qrcodegen-image"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99530e45ded4640c0eab5420fc60f9a0ec1be51a22e49cc8578b9a0d8be70712"
+dependencies = [
+ "base64",
+ "image",
+ "qrcodegen",
+]
+
[[package]]
name = "quick-xml"
version = "0.37.5"
@@ -2020,8 +2107,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
]
[[package]]
@@ -2031,7 +2128,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
- "rand_core",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
]
[[package]]
@@ -2043,13 +2150,22 @@ dependencies = [
"getrandom 0.2.16",
]
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.3",
+]
+
[[package]]
name = "rand_pcg"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e"
dependencies = [
- "rand_core",
+ "rand_core 0.6.4",
]
[[package]]
@@ -2058,7 +2174,7 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf2890aaef0aa82719a50e808de264f9484b74b442e1a3a0e5ee38243ac40bdb"
dependencies = [
- "rand_core",
+ "rand_core 0.6.4",
]
[[package]]
@@ -2203,8 +2319,8 @@ dependencies = [
"mysql",
"notify-rust",
"once_cell",
- "png",
- "rand",
+ "png 0.17.16",
+ "rand 0.8.6",
"redis",
"regex",
"scc",
@@ -2212,6 +2328,7 @@ dependencies = [
"serde_json",
"thread-priority",
"toml 0.8.23",
+ "totp-rs",
"ureq",
"uuid",
"walkdir",
@@ -2690,6 +2807,23 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+[[package]]
+name = "totp-rs"
+version = "5.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50e69a15e21b2ff22c415446983978bded3244195f17d59cb113551c1e806f91"
+dependencies = [
+ "base32",
+ "constant_time_eq",
+ "hmac",
+ "qrcodegen-image",
+ "rand 0.9.4",
+ "sha1 0.10.6",
+ "sha2",
+ "url",
+ "urlencoding",
+]
+
[[package]]
name = "tracing"
version = "0.1.41"
@@ -2783,6 +2917,12 @@ dependencies = [
"percent-encoding",
]
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
[[package]]
name = "utf8_iter"
version = "1.0.4"
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 2a823f76167..8dcec11bdef 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -48,6 +48,7 @@ once_cell = { version = "1.21" }
mysql = { git = "https://github.com/ZeWaka/rust-mysql-simple.git", tag = "v26.0.0", default-features = false }
dashmap = { version = "6.1", features = ["rayon", "serde"] }
uuid = { version = "1.18.1", features = ["v4"] }
+totp-rs = { version = "5.7.2", features = ["qr", "gen_secret"] }
[features]
# Use the native tls stack for the mysql db
diff --git a/rust/README.MD b/rust/README.MD
index c0d64781ed8..12973c84b22 100644
--- a/rust/README.MD
+++ b/rust/README.MD
@@ -9,6 +9,8 @@ It currently handles:
- Atmospherics (`milla`)
- DMM manipulation stuff (`mapmanip`)
+- MFA
+- Some other misc stuff like UUID generation
It also imports and tweaks the following [rust-g](https://github.com/tgstation/rust-g) features:
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index 4999a3825a2..80b7dd81482 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -1,7 +1,10 @@
mod jobs;
mod logging;
mod mapmanip;
+mod mfa;
mod milla;
+mod misc;
+// If its prefixed with rustlibs, its a port of the rust-g ones
mod rustlibs_dmi;
mod rustlibs_file;
mod rustlibs_git;
diff --git a/rust/src/mapmanip/procgen/mod.rs b/rust/src/mapmanip/procgen/mod.rs
index 14a189955aa..39a74a143a8 100644
--- a/rust/src/mapmanip/procgen/mod.rs
+++ b/rust/src/mapmanip/procgen/mod.rs
@@ -1,4 +1,4 @@
-use std::{collections::{HashMap, HashSet}, iter::Map};
+use std::collections::{HashMap, HashSet};
use super::core::GridMap;
diff --git a/rust/src/mfa/mod.rs b/rust/src/mfa/mod.rs
new file mode 100644
index 00000000000..8fd43345c1e
--- /dev/null
+++ b/rust/src/mfa/mod.rs
@@ -0,0 +1,57 @@
+use byondapi::value::ByondValue;
+use totp_rs::{Algorithm, Secret, TOTP};
+
+const ISSUER: &str = "Paradise Station";
+
+// This is a common method to ensure parameters stay the same between creation and verification
+fn instance_totp_manager(secret: String, accname: String) -> TOTP {
+ TOTP::new(
+ Algorithm::SHA1,
+ 6,
+ 1,
+ 30,
+ Secret::Encoded(secret.to_owned()).to_bytes().unwrap(),
+ Some(ISSUER.to_string()),
+ accname.to_owned(),
+ )
+ .unwrap() // who needs safety lmao
+}
+
+#[byondapi::bind]
+fn mfa_generate_secret() -> eyre::Result {
+ let mfa_secret = Secret::generate_secret().to_encoded().to_string();
+
+ Ok(ByondValue::new_str(mfa_secret)?)
+}
+
+#[byondapi::bind]
+fn mfa_generate_qr(secret: ByondValue, ckey: ByondValue) -> eyre::Result {
+ let secret_str = secret.get_string()?;
+ let ckey_str = ckey.get_string()?;
+
+ // Get the QR from our secret
+ let png_base64 = instance_totp_manager(secret_str, ckey_str)
+ .get_qr_base64()
+ .unwrap();
+
+ // Send it back
+ Ok(ByondValue::new_str(format!(
+ "data:image/png;base64,{png_base64}"
+ ))?)
+}
+
+#[byondapi::bind]
+fn mfa_verify_code(secret: ByondValue, code: ByondValue) -> eyre::Result {
+ // Code MUST be a string to avoid 0 prefixing issues
+ let secret_str = secret.get_string()?;
+ let code_str = code.get_string()?;
+
+ let totp = instance_totp_manager(secret_str, String::new());
+
+ // Check provided code
+ if totp.check_current(&code_str).unwrap_or(false) {
+ return Ok(ByondValue::new_num(1f32));
+ } else {
+ return Ok(ByondValue::new_num(0f32));
+ }
+}
diff --git a/rust/src/milla/tick.rs b/rust/src/milla/tick.rs
index 185a7ff8f9f..219360e914d 100644
--- a/rust/src/milla/tick.rs
+++ b/rust/src/milla/tick.rs
@@ -318,8 +318,8 @@ mod tests {
toxins_: Option,
sleeping_agent_: Option,
agent_b_: Option,
- hydrogen: Option,
- water_vapor: Option,
+ hydrogen_: Option,
+ water_vapor_: Option,
thermal_energy_: Option,
temperature_: Option,
}
@@ -333,8 +333,8 @@ mod tests {
toxins_: None,
sleeping_agent_: None,
agent_b_: None,
- hydrogen: None,
- water_vapor: None,
+ hydrogen_: None,
+ water_vapor_: None,
thermal_energy_: None,
temperature_: None,
}
diff --git a/rust/src/misc/mod.rs b/rust/src/misc/mod.rs
new file mode 100644
index 00000000000..c5622284289
--- /dev/null
+++ b/rust/src/misc/mod.rs
@@ -0,0 +1,8 @@
+use byondapi::value::ByondValue;
+use uuid::Uuid;
+
+#[byondapi::bind]
+fn misc_new_uuid() -> eyre::Result {
+ let uuid: String = Uuid::new_v4().to_string();
+ Ok(ByondValue::new_str(uuid)?)
+}
diff --git a/rust/src/rustlibs_http/mod.rs b/rust/src/rustlibs_http/mod.rs
index 36bc727a69c..eae7a4d8d8d 100644
--- a/rust/src/rustlibs_http/mod.rs
+++ b/rust/src/rustlibs_http/mod.rs
@@ -84,7 +84,7 @@ fn submit_request(prep: RequestPrep) -> Result {
// TODO: this is a sinful hack, rewrite this as soon as the module is stable on live
let response = match prep.req.send_bytes(&prep.body) {
Ok(r) => r,
- Err(ureq::Error::Status(code, r)) => r,
+ Err(ureq::Error::Status(_code, r)) => r,
Err(ureq::Error::Transport(t)) => {
let mut resp = Response {
status_code: 0,
diff --git a/rustlibs.dll b/rustlibs.dll
index 900266a8d29..9b6bf9f8076 100644
Binary files a/rustlibs.dll and b/rustlibs.dll differ
diff --git a/rustlibs_prod.dll b/rustlibs_prod.dll
index 8fc5140156d..2f647607d1d 100644
Binary files a/rustlibs_prod.dll and b/rustlibs_prod.dll differ
diff --git a/tools/ci/librustlibs_ci.so b/tools/ci/librustlibs_ci.so
index e94eda04090..3e8c161f22a 100644
Binary files a/tools/ci/librustlibs_ci.so and b/tools/ci/librustlibs_ci.so differ