/
config.R
391 lines (358 loc) · 13.5 KB
/
config.R
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
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
#' Configure the R environment for the course (including database information)
#' and provide (or cache) user information in ciphered form.
#'
#' Call these functions every time you need to get environment variables set,
#' like the URL, user and password of the MongoDB database used by the course.
#' @param url The URL of the encrypted file that contains the configuration
#' information.
#' @param data The fingerprint data in clear or ciphered form (in this case, the
#' string must start with "fingerprint=").
#' @param password The password to crypt, decrypt, lock or unlock the data.
#' @param cache The path to the file to use to store a cached version of these
#' data. Access to the database will be checked, and if it fails, the
#' configuration data are refreshed from the URL.
#' @param debug Do we issue debugging messages? By default, it is set according
#' to the `LEARNITDOWN_DEBUG` environment variable (yes, if this variable is not
#' `0`).
#' @param object An object to be encrypted, decrypted, locked or unlocked.
#' @param cipher The cryptography algorithm to use.
#' @param iv The initialization vector for the cipher.
#' @param serialize Do we serialize `object` before ciphering it (`TRUE` by
#' default)?
#' @param unserialize Do we unserialize the resulting object after deciphering
#' it (`TRUE` by default)?
#' @param base64 Do we encode/decode base64 the object (`FALSE` by default)?
#' @param url.encode Do we encode for URL (query) use (`FALSE` by default) ?
#' @param url.decode Do we decode URL before deciphering it (`FALSE` by
#' default)?
#' @param title The title of the dialog box prompting to sign out.
#' @param message The message of the dialog box prompting to sign out, or asking
#' for a password.
#' @param key The key that stores the password. It is advised to use something
#' like `course_year`, so that you can manage different passwords for the same
#' course given at different academic years. Using a key, the course password
#' must be entered only once. If not provided, the password is not stored. Note
#' also that the name of an environment variable could be used too for the key.
#' This is convenient on a server like RStudio Connect, for instance.
#' @param reset Should we reset the password (`FALSE` by default)?
#' @param ref1 Code to check the validity of the password
#' @param ref2 Second code to check password validity.
#'
#' @return Invisibly returns `TRUE` if success, or `FALSE` otherwise for
#' [config()]. The encrypted/decrypted object for [encrypt()] and [decrypt()],
#' or the locked/unlocked object for [lock()] and [unlock()].
#' The user information for [sign_in()].
#' @export
config <- function(url, password,
cache = file.path(tempdir(), ".learnitdown_config"),
debug = Sys.getenv("LEARNITDOWN_DEBUG", 0) != 0) {
debug <- isTRUE(debug)
# Make sure the environment variable is set correctly for debug
Sys.setenv(LEARNITDOWN_DEBUG = as.integer(debug))
# Set environment variables according to entries in an encrypted configuration
# file, and return the encrypted data, if it succeeds (test database access)
setenv <- function(file, password, debug) {
try({
conf_crypt <- readRDS(file)
conf <- decrypt(conf_crypt, password = password)
# Set environment variables
for (item in names(conf)) {
if (Sys.getenv(item) != "")
conf[[item]] <- NULL # Do not replace this item if already there
}
if (length(conf))
do.call(Sys.setenv, conf)
# Test the access to the MongoDB database, using env. vars
user <- Sys.getenv("MONGO_USER")
password <- Sys.getenv("MONGO_PASSWORD")
mongo_url <- Sys.getenv("MONGO_URL")
db <- Sys.getenv("MONGO_BASE")
if (debug)
message("Database URL: ", mongo_url, ", base: ", db)
mongo(collection = "learnr", db = db, url = glue(mongo_url))
# Return conf_crypt
conf_crypt
}, silent = TRUE)
}
# If the cache file is there, use it
if (file.exists(cache)) {
res <- setenv(cache, password = password, debug = debug)
if (!inherits(res, "try-error")) {
if (debug) {
message("Learnitdown configuration set from cache")
message("Database is responding")
}
return(invisible(TRUE))
}
}
# If no file cache, or an error occurs, try getting the config file from url
# First check if Internet connexion is alive
check_internet_access <- function() {
# IANA's test website
is_online <- function(site="http://example.com/") {
tryCatch({
readLines(site, n = 1)
TRUE
},
warning = function(w) invokeRestart("muffleWarning"),
error = function(e) FALSE)
}
# We test two different sites successively
res <- is_online("http://www.google.com")
if (!res)
res <- is_online("http://www.github.com")
if (!res)
stop("This computer does not seems to have access to the Internet, ", "
impossible to record events in the database ",
"(but they are saved on this computer for now).", call. = FALSE)
}
res <- try(check_internet_access(), silent = TRUE)
if (inherits(res, "try-error")) {
if (debug)
message("No access to the Internet: ", res)
return(invisible(structure(FALSE, error = res)))
}
res <- setenv(url(url), password = password, debug = debug)
if (inherits(res, "try-error")) {
if (debug)
message("Inaccessible or incorrect configuration or ",
"database not responding: ", res)
return(invisible(structure(FALSE, error = res)))
} else {
if (debug) {
message("Learnitdown configuration set from URL")
message("Database is responding")
}
# Save these data into the cache file
try(suppressWarnings(saveRDS(res, file = cache)), silent = TRUE)
return(invisible(TRUE))
}
}
#' @rdname config
#' @export
sign_in <- function(data, password, cipher = "aes-256-cbc", iv = NULL,
cache = file.path(tempdir(), ".learnitdown_user"),
debug = Sys.getenv("LEARNITDOWN_DEBUG", 0) != 0) {
debug <- isTRUE(debug)
if (!missing(data)) {
# If data = NULL, we delete fingerprint data (that is, user is signed out).
if (is.null(data)) {
unlink(cache, force = TRUE)
if (debug)
message("User information deleted (fingerprint cache).")
return(invisible(NULL))
}
# We set or replace fingerprint cache file
data <- as.character(data)
# If data are already encrypted, the string starts with "fingerprint="
if (substring(data, 1, 12) != "fingerprint=") {
# We encrypt these data
data <- encrypt(data, password = password, cipher = cipher, iv = iv,
serialize = FALSE, base64 = TRUE, url.encode = TRUE)
data <- paste0("fingerprint=", data)
}
writeBin(data, cache)
if (debug)
message("Fingerprint data cached.")
} else {
if (file.exists(cache)) {
data <- readBin(cache, "character")
if (debug)
message("Fingerprint retrieved from cache.")
} else {
# No data... nothing to do!
if (debug)
message("No fingerprint data.")
return(invisible(NULL))
}
}
# If password is provided, decrypt data
if (!missing(password)) {
data <- sub("^fingerprint=", "", data)
user_info_query <- try(decrypt(data, password = password, cipher = cipher,
iv = iv, unserialize = FALSE, base64 = TRUE, url.decode = TRUE),
silent = TRUE)
if (inherits(user_info_query, "try-error")) {
if (debug)
message("Error deciphering fingerprint: ", user_info_query)
return(invisible(NULL))
}
# Parse this query string
user_info <- parse_url(paste0("http://localhost?", user_info_query))$query
# Check: we must have a list with at least a login component
if (!is.list(user_info)) {
if (debug)
message("Expected a list for fingerprint data, got ", class(user_info))
return(invisible(NULL))
}
if (!"login" %in% names(user_info)) {
if (debug)
message("No 'login' in fingerprint user data")
return(invisible(NULL))
}
# Data should be OK
if (debug)
message("Successfully got user data from the fingerprint")
return(invisible(user_info))
} else {
return(invisible(data))
}
}
#' @rdname config
#' @export
sign_out <- function(title = "Signing out",
message = "Do you really want to sign out with learnitdown?",
cache = file.path(tempdir(), ".learnitdown_user"),
debug = Sys.getenv("LEARNITDOWN_DEBUG", 0) != 0) {
debug <- isTRUE(debug)
if (rstudioapi::isAvailable()) {
res <- rstudioapi::showQuestion(title, message)
} else {
res <- isTRUE(askYesNo(message))
}
if (res) {
unlink(cache, force = TRUE)
if (debug)
message("Signed out!")
}
res
}
serialized <- NULL # To avoid a spurious warning in R CMD check
#' @rdname config
#' @export
encrypt <- function(object, password, cipher = "aes-256-cbc", iv = NULL,
serialize = TRUE, base64 = FALSE, url.encode = FALSE) {
password <- as.character(password)
if (length(password) != 1)
stop("Use a single character string for the password")
if (!nchar(password))
stop("The password cannot be empty")
# Create a secure key from the password
key <- PKI.digest(charToRaw(password), "SHA256")
if (isTRUE(serialize)) {
serialize_con <- textConnection("serialized", "w", local = TRUE)
dput(object, file = serialize_con, control = "all")
close(serialize_con)
object <- charToRaw(paste(serialized, collapse = "\n"))
} else {
object <- charToRaw(as.character(object))
}
# Encrypt object
encrypted <- PKI.encrypt(object, key, cipher = cipher, iv = iv)
if (isTRUE(base64))
encrypted <- gsub("\n", "", base64_enc(encrypted))
if (isTRUE(url.encode))
encrypted <- URLencode(encrypted, reserved = TRUE)
encrypted
}
#' @rdname config
#' @export
decrypt <- function(object, password, cipher = "aes-256-cbc", iv = NULL,
unserialize = TRUE, base64 = FALSE, url.decode = FALSE) {
password <- as.character(password)
if (length(password) != 1)
stop("Use a single character string for the password")
if (!nchar(password))
stop("The password cannot be empty")
# Create a secure key from the password
key <- PKI.digest(charToRaw(password), "SHA256")
# Decode URL
if (isTRUE(url.decode))
object <- URLdecode(object)
# Decode base 64
if (isTRUE(base64))
object <- base64_dec(object)
# Decrypt
decrypted <- try(PKI.decrypt(object, key, cipher = cipher, iv = iv),
silent = TRUE)
if (inherits(decrypted, "try-error"))
return(NULL)
decrypted <- try(rawToChar(decrypted), silent = TRUE)
if (inherits(decrypted, "try-error"))
return(NULL)
# Possibly deserialize the object
if (isTRUE(unserialize)) {
decrypted <- dget(textConnection(decrypted))
}
decrypted
}
#' @rdname config
#' @export
lock <- function(object, password, key = "",
message = "Password for learnitdown:", reset = FALSE,
ref1 = NULL, ref2 = NULL) {
if (missing(password))
password <- .get_password(key, message = message, reset = reset,
ref1 = ref1, ref2 = ref2)
if (!nchar(password))
stop("Password unknown or not provided, cannot lock object")
encrypt(object, password = password, base64 = TRUE)
}
#' @rdname config
#' @export
unlock <- function(object, password, key = "",
message = "Password for learnitdown:", reset = FALSE,
ref1 = NULL, ref2 = NULL) {
if (missing(password))
password <- .get_password(key, message = message, reset = reset,
ref1 = ref1, ref2 = ref2)
if (!nchar(password))
stop("Password unknown or not provided, cannot unlock object")
decrypt(object, password = password, base64 = TRUE)
}
# Get the password for lock()/unlock()
.get_password <- function(key, message = "Password for learnitdown:",
store = TRUE, reset = FALSE, ref1 = NULL, ref2 = NULL) {
# First try to get it from environment variables (e.g., RStudio Connect)
if (nchar(key)) {
pass <- Sys.getenv(key)
token <- Sys.getenv("SATURN_TOKEN", unset = "")
if (nchar(token))
pass <- decrypt(pass, token, base64 = TRUE, url.decode = TRUE)
} else {
pass <- ""
}
if (!is.null(pass) && length(pass) && nchar(pass))
return(pass)
# Otherwise, try getting it from keyring
if (!isTRUE(reset) && nchar(key)) {
pass <- try(key_get(service = "org.sciviews.learnitdown",
username = key), silent = TRUE)
if (!inherits(pass, "try-error"))
return(pass)
}
# Ultimately, ask for it... (only in interactive mode)
if (interactive())
pass <- getPass(message)
if (is.null(pass))
pass <- ""
# If the password is not blank and ref1/ref2 provided, check it now
if (!is.null(pass) && length(pass) && nchar(pass) &&
!is.null(ref1) && length(ref1) && nchar(ref1)) {
if (!is.null(ref2)) {
res <- try(unlock(ref1, ref2) != pass, silent = TRUE)
if (inherits(res, "try-error"))
stop("Error while checking if password is correct!")
if (!is.null(res) && length(res) && res)
stop("Incorrect password!")
} else {
res <- try(unlock(ref1, pass) != "correct", silent = TRUE)
if (inherits(res, "try-error"))
stop("Error while checking if password is correct!")
if (!is.null(res) && length(res) && res)
stop("Incorrect password!")
}
}
# If the password and key are not blank, store it in keyring
if (nchar(pass) && nchar(key)) {
res <- try(key_set_with_value(service = "org.sciviews.learnitdown",
username = key, password = pass), silent = TRUE) # We prefer no error
# message in case it does not work (then we just will have to provide the
# password every time we need it)
if (inherits(res, "try-error")) {
warning("The password cannot be stored in the keyring backend")
attr(pass, "error") <- res
}
}
pass
}