-
-
Notifications
You must be signed in to change notification settings - Fork 444
/
Copy pathmfa.dm
293 lines (238 loc) · 9.05 KB
/
mfa.dm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
#define CHECK_MFA_ENABLED if(!CONFIG_GET(flag/mfa_enabled)) return TRUE
/// Checks the MFA for the client, then logs them in if they succeed
/// Returns true if login successful, false otherwise
/// The argument is TRUE if the user should be promted, false if not, useful for on login when the client may not be
/// prepared for the query
/client/proc/mfa_check(allow_query = TRUE)
CHECK_MFA_ENABLED
if(mfa_check_cache())
return TRUE
else // Need to run MFA
if(allow_query)
INVOKE_ASYNC(src, PROC_REF(mfa_query_login)) // Don't want to hang while the user inputs their TOTP code
else
to_chat(src, span_userdanger("New connection detected, use the readmin verb to authenticate!"))
return FALSE
/// Check if the login from this location is from an existing session
/client/proc/mfa_check_cache()
CHECK_MFA_ENABLED
var/datum/admins/tmp_holder = GLOB.permissions.admin_datums[ckey] || GLOB.permissions.deadmins[ckey]
if(tmp_holder && tmp_holder.cid_cache == computer_id && tmp_holder.ip_cache == address)
return TRUE
var/datum/DBQuery/query_mfa_check = SSdbcore.NewQuery(
"SELECT COUNT(1) FROM [format_table_name("mfa_logins")] WHERE ckey = :ckey AND ip = INET_ATON(:address) AND cid = :cid AND datetime > current_timestamp() - INTERVAL 30 DAY;",
list("ckey" = ckey, "address" = address, "cid" = computer_id)
)
var/success = (query_mfa_check.warn_execute() && query_mfa_check.NextRow() && text2num(query_mfa_check.item[1]) > 0) // Check if they have connected before from this IP/CID
qdel(query_mfa_check)
return success
/// Queries the user for their MFA credentials, then logs them in if they are correct
/client/proc/mfa_query_login()
CHECK_MFA_ENABLED
if(mfa_query())
mfa_login()
/// Asks the user for their 2FA code
/client/proc/mfa_query()
CHECK_MFA_ENABLED
var/datum/DBQuery/query_totp_seed = SSdbcore.NewQuery(
"SELECT totp_seed FROM [format_table_name("player")] WHERE ckey = :ckey",
list("ckey" = ckey)
)
if(!query_totp_seed.warn_execute())
qdel(query_totp_seed)
var/msg = "SQL Error getting TOTP seed for [ckey]"
message_admins(msg)
log_admin(msg)
return FALSE
if(!query_totp_seed.NextRow())
qdel(query_totp_seed)
var/msg = "Cannot find DB entry for [ckey] who is attempting to use MFA, this shouldn't be possible."
message_admins(msg)
log_admin(msg)
return FALSE
var/seed = query_totp_seed.item[1]
qdel(query_totp_seed)
if(!seed)
return mfa_enroll()
var/code = input(src, "Please enter your authentication code", "MFA Check") as null|num
if(code)
var/json_codes = rustg_hash_generate_totp_tolerance(seed, "1")
if(findtext(json_codes, "ERROR") != 0) // Something went wrong, exit
var/msg = "Error with TOTP: [json_codes]"
message_admins(msg)
log_admin(msg)
return FALSE
var/generated_codes = json_decode(json_codes)
if(num2text(code) in generated_codes)
return TRUE
var/response = tgui_alert(src, "How would you like to proceed?", "Authentication Error", list("Retry TOTP", "Backup Code", "Cancel"))
if(response == "Cancel")
return
if(response == "Retry TOTP")
return mfa_query()
else if(response == "Backup Code")
if(tgui_alert(src, "Using the backup code will forget all previous logins and require re-enrolling in MFA, Do you wish to continue?", "Confirmation", list("Cancel", "Yes")) != "Yes")
return mfa_query()
return mfa_backup_query()
else
CRASH("INVALID RESPONSE TO QUERY")
/// Enrolls a user the in MFA system, assigning them a TOTP seed and backup code
/client/proc/mfa_enroll()
CHECK_MFA_ENABLED
if(IsAdminAdvancedProcCall())
var/msg = " has tried to elevate permissions!"
message_admins("[key_name_admin(usr)][msg]")
log_admin("[key_name(usr)][msg]")
return FALSE
var/list/base32lookup = list(
"A" = 0,
"B" = 1,
"C" = 2,
"D" = 3,
"E" = 4,
"F" = 5,
"G" = 6,
"H" = 7,
"I" = 8,
"J" = 9,
"K" = 10,
"L" = 11,
"M" = 12,
"N" = 13,
"O" = 14,
"P" = 15,
"Q" = 16,
"R" = 17,
"S" = 18,
"T" = 19,
"U" = 20,
"V" = 21,
"W" = 22,
"X" = 23,
"Y" = 24,
"Z" = 25, // 0 and 1 skipped due to similarty to O and I, RFC 4648
"2" = 26,
"3" = 27,
"4" = 28,
"5" = 29,
"6" = 30,
"7" = 31
)
var/code_b32 = ""
for(var/i = 0; i<16; i++) // Generate 16 character base 32 number
code_b32 += pick(base32lookup)
var/code_b2 = ""
for(var/char in splittext(code_b32, ""))
code_b2 += num2text(base32lookup[char], 5, 2)
var/code_b16 = ""
for(var/byte in splittext(code_b2,regex(@"([01]{4})")))
if(byte == "")
continue
code_b16 += num2text(text2num(byte, 2), 1, 16)
var/alphabet = splittext("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", "")
var/raw_backup = ""
for(var/i = 0; i < 16; i++) // Generate 16 character base 32 number
raw_backup += pick(alphabet)
var/backup_hash = rustg_hash_string(RUSTG_HASH_SHA512, raw_backup)
var/mfa_uri = "otpauth://totp/[ckey]?secret=[code_b32]&issuer=Yogstation13"
var/qr_image = "<img src=\"https://api.qrserver.com/v1/create-qr-code/?data=[url_encode(mfa_uri)]&size=200x200\" />"
var/heading = "<h2>2FA Setup</h2>"
var/instructions = "Use the below QR Code or TOTP code in a 2FA app like Google Authenticator."
var/codes = "TOTP CODE: [code_b32]<br>Backup code for if you lose your 2FA device: [raw_backup]"
src << browse("<HTML><HEAD><meta charset='UTF-8'><TITLE>QR Code</TITLE></HEAD><BODY>[heading][instructions]<br>[qr_image]<br>[codes]</BODY></HTML>", "window=MFA_QR")
while(TRUE)
var/code = input(src, "Please verify your authentication code", "MFA Check") as null|num
if(code)
var/json_codes = rustg_hash_generate_totp_tolerance(code_b16, "1")
if(findtext(json_codes, "ERROR") != 0) // Something went wrong, exit
var/msg = "Error with TOTP: [json_codes]"
message_admins(msg)
log_admin(msg)
return FALSE
var/generated_codes = json_decode(json_codes)
if(num2text(code) in generated_codes)
break
else
return FALSE
var/datum/DBQuery/query_set_totp_seed = SSdbcore.NewQuery(
"UPDATE [format_table_name("player")] SET totp_seed = :totp_seed, mfa_backup = :mfa_backup WHERE ckey = :ckey",
list("totp_seed" = code_b16, "mfa_backup" = backup_hash, "ckey" = ckey)
)
var/result = query_set_totp_seed.warn_execute()
qdel(query_set_totp_seed)
return result
/// Asks the user for their backup codes, then resets the login and re-enrolls the user on success
/client/proc/mfa_backup_query()
CHECK_MFA_ENABLED
var/mfa_backup = input(src, "Please enter your authentication code", "MFA Check") as null|text
if(!mfa_backup)
return
var/datum/DBQuery/query_mfa_backup = SSdbcore.NewQuery(
"SELECT COUNT(1) FROM [format_table_name("player")] WHERE ckey = :ckey AND mfa_backup = :code",
list("ckey" = ckey, "code" = rustg_hash_string(RUSTG_HASH_SHA512, mfa_backup))
)
if(!query_mfa_backup.warn_execute() || !query_mfa_backup.NextRow())
qdel(query_mfa_backup)
var/msg = "Unable to fetch backup codes for [ckey]!"
message_admins(msg)
log_admin(msg)
to_chat(src, span_warning("Unable to fetch batckup codes"))
return FALSE
var/authed = query_mfa_backup.item[1] > 0
qdel(query_mfa_backup)
if(authed)
var/msg = "[ckey] logged in with their backup code!"
message_admins(msg)
log_admin(msg)
mfa_reset(ckey)
return mfa_enroll()
else
to_chat(src, span_warning("Failed to validate backup code"))
return FALSE
/// Reset MFA, clear sessions and login credentials
/proc/mfa_reset(ckey, session_only = FALSE)
CHECK_MFA_ENABLED
if(IsAdminAdvancedProcCall())
var/msg = " has tried to elevate permissions!"
message_admins("[key_name_admin(usr)][msg]")
log_admin("[key_name(usr)][msg]")
return FALSE
var/datum/DBQuery/query_clear_mfa = SSdbcore.NewQuery(
"DELETE FROM [format_table_name("mfa_logins")] WHERE ckey = :ckey",
list("ckey" = ckey)
)
query_clear_mfa.warn_execute()
qdel(query_clear_mfa)
if(!session_only)
query_clear_mfa = SSdbcore.NewQuery(
"UPDATE [format_table_name("player")] SET totp_seed = NULL, mfa_backup = NULL WHERE ckey = :ckey",
list("ckey" = ckey)
)
query_clear_mfa.warn_execute()
qdel(query_clear_mfa)
/client/proc/mfa_login()
CHECK_MFA_ENABLED
if(IsAdminAdvancedProcCall())
var/msg = " has tried to elevate permissions!"
message_admins("[key_name_admin(usr)][msg]")
log_admin("[key_name(usr)][msg]")
return
if(tgui_alert(src, "Do you wish to remember this connection?", "Remember Me", list("Yes", "No")) == "Yes")
var/datum/DBQuery/mfa_addverify = SSdbcore.NewQuery(
"INSERT INTO [format_table_name("mfa_logins")] (ckey, ip, cid) VALUE (:ckey, INET_ATON(:address), :cid)",
list("ckey" = ckey, "address" = address, "cid" = computer_id)
)
if(!mfa_addverify.Execute())
qdel(mfa_addverify)
var/msg = "Failed to add login info for [ckey], they will be unable to login"
message_admins(msg)
log_admin(msg)
return
qdel(mfa_addverify)
var/datum/admins/tmp_holder = GLOB.permissions.admin_datums[ckey] || GLOB.permissions.deadmins[ckey]
if(tmp_holder)
// These values are cached even if the user says not to remember the session, but are only used if the DB is down during admin loading
tmp_holder.cid_cache = computer_id
tmp_holder.ip_cache = address
tmp_holder.activate()
#undef CHECK_MFA_ENABLED