From 54b96628968ec9fae71bf24147ab278280bfc9b2 Mon Sep 17 00:00:00 2001 From: Falk Mielke Date: Fri, 5 Sep 2025 12:41:57 +0200 Subject: [PATCH 01/11] a tutorial on using R `keyring` --- content/tutorials/r_keyring/index.md | 409 ++++++++++++++ .../tutorials/r_keyring/keyring_tutorial.qmd | 502 ++++++++++++++++++ ...search-institute-for-nature-and-forest.csl | 298 +++++++++++ 3 files changed, 1209 insertions(+) create mode 100644 content/tutorials/r_keyring/index.md create mode 100644 content/tutorials/r_keyring/keyring_tutorial.qmd create mode 100644 content/tutorials/r_keyring/research-institute-for-nature-and-forest.csl diff --git a/content/tutorials/r_keyring/index.md b/content/tutorials/r_keyring/index.md new file mode 100644 index 000000000..7117bb6e9 --- /dev/null +++ b/content/tutorials/r_keyring/index.md @@ -0,0 +1,409 @@ + + +--- +title: "The `keyring` package: We can do better than `*******`." +description: "Using your system keyring for storing and accessing secrets from within R via the `keyring` package" +date: "2025-09-05" +authors: [falkmielke] +categories: ["development", "r"] +tags: ["secrets", "keyring", "passwords", "credentials", "r", "scripting"] +number-sections: true +link-citations: true +params: + math: true +format: + hugo-md: + toc: false + preserve_yaml: true + html-math-method: katex +--- + +What do the software "giants" Cisco, Ivanti, and Atlassian all have in common? +They all (repeatedly) raised negative attention in the past by [pushing hardcoded, unencrypted credentials to the public](https://www.bleepingcomputer.com/search/?q=hardcoded+credentials). + +This brief tutorial will help you to not be the poor intern who is rightfully expelled for such a fatal beginners' mistake. + +The programming environment we will use is R. +The package of choice is [`keyring`](https://keyring.r-lib.org/index.html), but it will be spiced up with some extra precautions. + +# How it Works: the Diary + +Imagine that you are age fourteen again, sitting in your room in the evening and reflecting the day, still aroused by that encounter at school with that attractive same-aged conspecific who subtly expressed interest in you after lunchbreak. +*First love is tough.* + +No way you would tell anyone about this: your friends would lay on the floor giggling as is habit at the age; and of course not your parents (how embarassing would that be). +It is all new, emotions are going rampage - +best time to keep them private. + +But to sort these secret new emotions, it would be good to put them to paper. +**Let us see what options you have to store our secret thoughts.** + +- 1. You could just write them on a *plain paper* on your desk. + +Good that they are sorted. +Bad that anyone can read them: mum, pa, and that annoying little brother. + +This is what we call *"hardcoded, unencrypted"* storage, and it is precicely what needs to be avoided. + +- 1. You can write them in a *diary with a lock*. + +Better. +Yet if someone has the key, or can xray-view the text in the diary, your secrets are still exposed. + +- 1. You can write in a secret language (*encryption*), in a locked diary. + +It turns out that your computer already has such a diary place, a safe space where it can store secrets and credentials. +It is called the **"system keyring"**. +The system keyring lives on your [operating system](https://keyring.r-lib.org/index.html), and can be used to temporarily or permanently store any secrets in working memory. + +Storing secrets in RAM is like the plain paper option above, so smart computer engineers also gave us the lock and the secret language. + +And here is where it gets useful: +you can manage the keyring from within *R*, to automate secrecy in your scripts. +Guess what: the package is called `keyring`. + +``` r +library("keyring") +library("getPass") # for some additional tweaks, see below. +``` + +Some further constraints: + +- You do not want to get caught/observed writing your secrets, prior to encryption, which is why we will also use `getPass` in some situations. +- Just like a diary, your secrets can potentially get lost in any of the options above, they are more or less volatile / persistent. Keep that in mind, and don't complain to me if you loose one. + +# The Naïve Keyring Failure + +## Simple Usage + +The `keyring` package [is simple to use](https://keyring.r-lib.org/index.html#usage), and your system keyring is normally in place. + +For most common use, you might want to get away with + +``` r +keyring::key_set("diary-entry-1") +keyring::key_get("diary-entry-1") +``` + +And, admitted, I used keyring like so for quite a while now. +However, then I noticed a serious problem, which is there by design: + +{{% callout warning %}} + +- The default keyring is not password-locked. +- The secrets persist across R sessions, potentially even across reboots. + +{{% /callout %}} + +This is an unfortunate combination: it means that if you use `key_set`, and enter your password, you are in a worse situation than before. +You just created a diary without a lock and left it on your desk, so that everyone who likes can just enter your room and read that good-looking Mickey gave you a complement about your new braces :blush:. + +In computer terms: anyone who comes to your computer can access the password in a new R session with `key_get`. +If I am not mistaken, passwords in some keyrings even persist reboots, yet I did not test the default one. + +What a lousy vault. +Laxity killed secrecy. + +## Creating a Custom Keyring + +Lesson learned: you **always** want to create a custom keyring. +And this is actually what the authors of `keyring` correctly explain [in the "Usage" section](https://keyring.r-lib.org/index.html#usage). + +``` r +keyring_create(keyring = "vault") +``` + +On my computer, in a fresh R terminal, this initiated three things which happen in a row. + +- 1. Asks the user for a password on the terminal. +- 1. Opens a (very neat) popup window which asks for password and confirmation, indicating password strength. +- 1. Issues a warning that `Password ignored, will be read interactively`. + +This might be confusing, and three passwords seem one too much. +I guess this is historic burden, or compatibility for headless systems, and found that in my workflow I can safely get rid of points (i) and (iii) by instead using the following: + +``` r +suppressWarnings(keyring::keyring_create(keyring = "vault", password = "")) +``` + +The first password is given as empty string, and thus not prompted, and the ignorant warning message gets suppressed. + +This is the keyring which we can use to store our secrets. +It is locked and unlocked with a master password, and any secrets stored inside will be encrypted. +Keep in mind that your keyring will survive a reboot, unless you delete it. + +More on all that, below. + +# Managing Keys + +## Stowing Secrets + +Now that we have a place to stow our secrets, let's use it. + +``` r +key_set( + service = "diary", + username = "mickey", + keyring = "vault" +) +``` + +Note that the function takes a `service`, which I think of as a sort of category within a `keyring`. +Turns out that I can use my vault to store different kind of things: passwords, money, photos of ex girlfriends... +Here, I named it `diary`, for the sake of analogy. + +Then, there is the `username`, which makes sense in a service created for storing username-password credentials. +You ask the "diary"-service of your "vault"-keyring for the password of/to "mickey". +Generally, think of it as a label, or a secret message[^1], which you provide to your vault when you want to retrieve your secret... + +## Recovering Secrets + +### Naïve Getting + +And that would work like so: + +``` r +key_get("diary", "mickey", keyring = "vault") +``` + +Wait... + +OMG... + +Did my R console just print the secret we entered earlier?! + +### Invisible Getting + +You might consider using `invisible(key_get(...))`, or even generally: + +``` r +key_get <- function(...) invisible(keyring::key_get(...)) +``` + +*Welcome to next level paranoia.* + +However, this indicates what I think `keyring` is actually made for: +you actually want to assign your secrets to some variable, temporarily, as in + +``` r +database_connection <- DBI::dbConnect( + RPostgres::Postgres(), + dbname = "my_database", + host = "localhost", + port = 5439, + user = "mickey", + password = key_get("diary", "mickey", keyring = "vault") +) # just an example... +``` + +### Scripted Getting: Interactive Pitfalls + +Conclusion: + +{{% callout note %}} + +The main use case of `keyring` are scripts and code chains which repeatedly use secrets (because, if it was not repeatedly, `getPass` would be fine). +In these situations, `keyring` helps you to avoid te worse options of (a) hardcoding your secrets or (b) having to type them over and over again. + +{{% /callout %}} + +However, that did not quite work out for me. +The reason lies in the distinction between R's `interactive()` and non-interactive execution mode for using `key_set`. + +What does *(non-)interactive* mean? + +Say you have an automated process: a script which will write all the secret events of the day into your diary. +Or a script which performs data processes on an SQL database. +The script is called `secrecy_processing.R`. + +The working of `key_set(...)` depends on whether you run the code inside that script from a system terminal, like in `Rscript secrecy_processing.R`, or from the R console or another script, e.g. `source('secrecy_processing.R')`. +The second situation, *interactive mode*, is equivalend to what RStudio does, and works conveniently for most people. + +Yet in case you have an automated process, one that you run from a terminal or via a `cronjob`, `key_set` fails because it takes continuous input: it will just use the next line that it receives and think it is the password. + +This is a weird quirk[^2] which has caused me some headache, but at least I [learned about R's `interactive` mode](https://stackoverflow.com/a/27114322) on the way. + +Here is the workaround: `keyring` brings a function to `key_set_with_value` with a password argument, which can be customized. + +``` r +# takes the arguments `service`, `username` and `keyring` +key_set <- function(...) { + # check whether this is executed in interactive mode + if (interactive()) { + keyring::key_set(...) + } else { + # when run as a script, getPass will wait for user input + keyring::key_set_with_value( + ..., + password = getPass::getPass("Please enter your secret:") + ) + } +} + +# example usage +key_set( + service = "diary", + username = "mickey", + keyring = "vault" +) + +# print(key_get("diary", "mickey", keyring = "vault")) +``` + +This could be solved with `readLines("stdin", n = 1)`, yet then the user input is visible in plain text during typing. +The terminal solution I found is the R library `getPass`; however, you could also search for a dialog box option to enter the password, or a confirmation mechanism. + +Another option would be to use [`configr`](https://github.com/Miachol/configr) and store a config file with secrets in a system vault (e.g. with ["tomb"](https://dyne.org/tomb/)). + +Get creative. + +### Locking and Unlocking + +You might notice that, once your keyring is created, the master password is never asked again. +*What good is a vault for if you do not lock the door?* + +At the end of your script, or even better: after finishing an operation, you might want to lock your keyring. + +``` r +keyring::keyring_lock(keyring = "vault") +# keyring::keyring_unlock(keyring = "vault") +``` + +This is critical: +Because you work on the **system keyring**, your keyring stays open even after the R sessions closed. + +Normally, it should be locked upon reboot. +However, it is good practice to lock your keyrings whenever you procedure using it has finished. + +{{% callout note %}} + +**Always** make sure that your keyring is locked when the work is finished. + +{{% /callout %}} + +It might be even better practice to already schedule the closing of your keyring at the moment you open it. +Just so you don't forget to leave your door open when you leave. + +``` r +keyring_unlock <- function(keyring = NULL, ...) { + + # unlock the keyring + keyring::keyring_unlock(keyring = keyring, ...) + + # plan a procedure to lock it when the R session ends + # https://stackoverflow.com/a/41179916 + reg.finalizer( + .GlobalEnv, + function(e) { + keyring::keyring_lock(keyring = keyring) + message(glue::glue("Keyring `{keyring}` locked.")) + }, + onexit = TRUE # I wonder whether you could also set a timer... + ) + + return(invisible(NULL)) +} + +keyring_unlock(keyring = "vault") +``` + +# Cleanup + +After using this for a while, you might begin to hear a sort of metallic rattling noise whenever you start moving. +These are all the keys you are bringing along on your dear old `keyring`, but forgot about. + +Help is near: + +``` r +key_list() +key_list(keyring = "vault") +``` + +To delete the naïve key we created above: + +``` r +key_delete("diary-entry-1") +``` + +And, for even more paranoia, delete your entire keyring: + +``` r +keyring_delete("vault") +``` + +This will prompt you if the keyring is not empty, so in a scripted situation, you would want to delete all keys prior to deleting the empty keyring. + +# Summary + +Never ever fall to the temptation of hardcoding a password, anywhere. +It almost inevitably will cause you trouble later on. + +The longer-than-intended text above captures my own musings with the great `keyring` package for R. +I sincerely hope that it has made your code safer. +I certainly would have loved to know about all those things back then when puberty hit me. + +# TL;DR + +For the young and impatient, here are all the tricks from above in one code block. + +``` r +library("keyring") +library("getPass") + + +keyring_create <- function(...) { + suppressWarnings( + keyring::keyring_create(..., password = "") + ) +} + +# reduce the chance of accidental printout of a key +key_get <- function(...) invisible(keyring::key_get(...)) + +# setting keys in a scripted environment +# takes the arguments `service`, `username` and `keyring` +key_set <- function(...) { + # check whether this is executed in interactive mode + if (interactive()) { + keyring::key_set(...) + } else { + # when run as a script, getPass will wait for user input + keyring::key_set_with_value( + ..., + password = getPass::getPass("Please enter your secret:") + ) + } +} + + +# unlock a keyring, and schedule locking for when the R session ends +keyring_unlock <- function(keyring = NULL, ...) { + + # unlock the keyring + keyring::keyring_unlock(keyring = keyring, ...) + + # plan a procedure to lock it when the R session ends + # https://stackoverflow.com/a/41179916 + reg.finalizer( + .GlobalEnv, + function(e) { + keyring::keyring_lock(keyring = keyring) + message(glue::glue("Keyring `{keyring}` locked.")) + }, + onexit = TRUE # I wonder whether you could also set a timer... + ) + + return(invisible(NULL)) +} +``` + +- Never hard-code or store plain text secrets. +- Always create a custom keyring. +- Lock your keyring from within each process which uses it. +- Inspect and clean up you keyrings regularly. + +*Stay safe, everyone!* + +[^1]: This `username` itself is subject to secrecy, in a sense that you might increase security in certain situations by not hardcoding it (you could ask the user "which secret would you like to retrieve", with another `getPass`, stay tuned to see how that works). + +[^2]: Note that scripted `key_set` is probably not in the intention of the creators of `keyring`, relying on system keyring persistence across sessions; yet I for my part like to delete all secrets from memory after a process has finished. diff --git a/content/tutorials/r_keyring/keyring_tutorial.qmd b/content/tutorials/r_keyring/keyring_tutorial.qmd new file mode 100644 index 000000000..274c89921 --- /dev/null +++ b/content/tutorials/r_keyring/keyring_tutorial.qmd @@ -0,0 +1,502 @@ +--- +title: "" +author: "" +date: "" +link-citations: true +csl: '`r cslfile <- file.path("./research-institute-for-nature-and-forest.csl"); download.file("https://github.com/inbo/styles/raw/master/research-institute-for-nature-and-forest.csl", cslfile); cslfile`' +number-sections: true +format: + html: + toc: true + html-math-method: katex + variant: -tex_math_dollars+tex_math_single_backslash + embed-resources: true + hugo-md: + output-file: "index.md" + toc: false + preserve_yaml: false + maths: true + variant: -tex_math_dollars+tex_math_double_backslash-yaml_metadata_block-pandoc_title_block +--- + +```{=markdown} +--- +title: "The `keyring` package: We can do better than `*******`." +description: "Using your system keyring for storing and accessing secrets from within R via the `keyring` package" +date: "2025-09-05" +authors: [falkmielke] +categories: ["development", "r"] +tags: ["secrets", "keyring", "passwords", "credentials", "r", "scripting"] +number-sections: true +link-citations: true +params: + math: true +format: + hugo-md: + toc: false + preserve_yaml: true + html-math-method: katex +--- +``` + + +What do the software "giants" Cisco, Ivanti, and Atlassian all have in common? +They all (repeatedly) raised negative attention in the past by [pushing hardcoded, unencrypted credentials to the public](https://www.bleepingcomputer.com/search/?q=hardcoded+credentials). + +This brief tutorial will help you to not be the poor intern who is rightfully expelled for such a fatal beginners' mistake. + + +The programming environment we will use is R. +The package of choice is [`keyring`](https://keyring.r-lib.org/index.html), but it will be spiced up with some extra precautions. + + +# How it Works: the Diary + +Imagine that you are age fourteen again, sitting in your room in the evening and reflecting the day, still aroused by that encounter at school with that attractive same-aged conspecific who subtly expressed interest in you after lunchbreak. +*First love is tough.* + +No way you would tell anyone about this: your friends would lay on the floor giggling as is habit at the age; and of course not your parents (how embarassing would that be). +It is all new, emotions are going rampage - +best time to keep them private. + + +But to sort these secret new emotions, it would be good to put them to paper. +**Let us see what options you have to store our secret thoughts.** + +- (1) You could just write them on a *plain paper* on your desk. + +Good that they are sorted. +Bad that anyone can read them: mum, pa, and that annoying little brother. + +This is what we call *"hardcoded, unencrypted"* storage, and it is precicely what needs to be avoided. + + +- (2) You can write them in a *diary with a lock*. + +Better. +Yet if someone has the key, or can xray-view the text in the diary, your secrets are still exposed. + + +- (3) You can write in a secret language (*encryption*), in a locked diary. + + +It turns out that your computer already has such a diary place, a safe space where it can store secrets and credentials. +It is called the **"system keyring"**. +The system keyring lives on your [operating system](https://keyring.r-lib.org/index.html), and can be used to temporarily or permanently store any secrets in working memory. + +Storing secrets in RAM is like the plain paper option above, so smart computer engineers also gave us the lock and the secret language. + + +And here is where it gets useful: +you can manage the keyring from within *R*, to automate secrecy in your scripts. +Guess what: the package is called `keyring`. + +```{r libraries} +#| eval: false +library("keyring") +library("getPass") # for some additional tweaks, see below. +``` + + +Some further constraints: + +- You do not want to get caught/observed writing your secrets, prior to encryption, which is why we will also use `getPass` in some situations. +- Just like a diary, your secrets can potentially get lost in any of the options above, they are more or less volatile / persistent. Keep that in mind, and don't complain to me if you loose one. + + +# The Naïve Keyring Failure +## Simple Usage + +The `keyring` package [is simple to use](https://keyring.r-lib.org/index.html#usage), and your system keyring is normally in place. + +For most common use, you might want to get away with + +```{r get-set} +#| eval: false +keyring::key_set("diary-entry-1") +keyring::key_get("diary-entry-1") + +``` + + +And, admitted, I used keyring like so for quite a while now. +However, then I noticed a serious problem, which is there by design: + + +```{=markdown} +{{% callout warning %}} +``` +- The default keyring is not password-locked. +- The secrets persist across R sessions, potentially even across reboots. + +```{=markdown} +{{% /callout %}} +``` + +This is an unfortunate combination: it means that if you use `key_set`, and enter your password, you are in a worse situation than before. +You just created a diary without a lock and left it on your desk, so that everyone who likes can just enter your room and read that good-looking Mickey gave you a complement about your new braces :blush:. + + +In computer terms: anyone who comes to your computer can access the password in a new R session with `key_get`. +If I am not mistaken, passwords in some keyrings even persist reboots, yet I did not test the default one. + + +What a lousy vault. +Laxity killed secrecy. + + +## Creating a Custom Keyring + +Lesson learned: you **always** want to create a custom keyring. +And this is actually what the authors of `keyring` correctly explain [in the "Usage" section](https://keyring.r-lib.org/index.html#usage). + +```{r create-keyring} +#| eval: false +keyring_create(keyring = "vault") +``` + +On my computer, in a fresh R terminal, this initiated three things which happen in a row. + +- (i) Asks the user for a password on the terminal. +- (ii) Opens a (very neat) popup window which asks for password and confirmation, indicating password strength. +- (iii) Issues a warning that `Password ignored, will be read interactively`. + + +This might be confusing, and three passwords seem one too much. +I guess this is historic burden, or compatibility for headless systems, and found that in my workflow I can safely get rid of points (i) and (iii) by instead using the following: + +```{r better-create-keyring} +#| eval: false +suppressWarnings(keyring::keyring_create(keyring = "vault", password = "")) +``` + +The first password is given as empty string, and thus not prompted, and the ignorant warning message gets suppressed. + + + +This is the keyring which we can use to store our secrets. +It is locked and unlocked with a master password, and any secrets stored inside will be encrypted. +Keep in mind that your keyring will survive a reboot, unless you delete it. + +More on all that, below. + + +# Managing Keys + +## Stowing Secrets + +Now that we have a place to stow our secrets, let's use it. + +```{r set-key} +#| eval: false +key_set( + service = "diary", + username = "mickey", + keyring = "vault" +) + +``` + +Note that the function takes a `service`, which I think of as a sort of category within a `keyring`. +Turns out that I can use my vault to store different kind of things: passwords, money, photos of ex girlfriends... +Here, I named it `diary`, for the sake of analogy. + + +Then, there is the `username`, which makes sense in a service created for storing username-password credentials. +You ask the "diary"-service of your "vault"-keyring for the password of/to "mickey". +Generally, think of it as a label, or a secret message[^1], which you provide to your vault when you want to retrieve your secret... + +[^1]: This `username` itself is subject to secrecy, in a sense that you might increase security in certain situations by not hardcoding it (you could ask the user "which secret would you like to retrieve", with another `getPass`, stay tuned to see how that works). + + +## Recovering Secrets + +### Naïve Getting + +And that would work like so: + +```{r get-key} +#| eval: false +key_get("diary", "mickey", keyring = "vault") +``` + +Wait... + +OMG... + +Did my R console just print the secret we entered earlier?! + + +### Invisible Getting + +You might consider using `invisible(key_get(...))`, or even generally: +```{r invisible-get-key} +#| eval: false +key_get <- function(...) invisible(keyring::key_get(...)) +``` + +*Welcome to next level paranoia.* + + +However, this indicates what I think `keyring` is actually made for: +you actually want to assign your secrets to some variable, temporarily, as in + +```{r scripted-use} +#| eval: false + +database_connection <- DBI::dbConnect( + RPostgres::Postgres(), + dbname = "my_database", + host = "localhost", + port = 5439, + user = "mickey", + password = key_get("diary", "mickey", keyring = "vault") +) # just an example... + +``` + + +### Scripted Getting: Interactive Pitfalls + +Conclusion: + +```{=markdown} +{{% callout note %}} +``` +The main use case of `keyring` are scripts and code chains which repeatedly use secrets (because, if it was not repeatedly, `getPass` would be fine). +In these situations, `keyring` helps you to avoid te worse options of (a) hardcoding your secrets or (b) having to type them over and over again. + +```{=markdown} +{{% /callout %}} +``` + +However, that did not quite work out for me. +The reason lies in the distinction between R's `interactive()` and non-interactive execution mode for using `key_set`. + + +What does *(non-)interactive* mean? + +Say you have an automated process: a script which will write all the secret events of the day into your diary. +Or a script which performs data processes on an SQL database. +The script is called `secrecy_processing.R`. + + +The working of `key_set(...)` depends on whether you run the code inside that script from a system terminal, like in `Rscript secrecy_processing.R`, or from the R console or another script, e.g. `source('secrecy_processing.R')`. +The second situation, *interactive mode*, is equivalend to what RStudio does, and works conveniently for most people. + + +Yet in case you have an automated process, one that you run from a terminal or via a `cronjob`, `key_set` fails because it takes continuous input: it will just use the next line that it receives and think it is the password. + +This is a weird quirk[^2] which has caused me some headache, but at least I [learned about R's `interactive` mode](https://stackoverflow.com/a/27114322) on the way. + +[^2]: Note that scripted `key_set` is probably not in the intention of the creators of `keyring`, relying on system keyring persistence across sessions; yet I for my part like to delete all secrets from memory after a process has finished. + + +Here is the workaround: `keyring` brings a function to `key_set_with_value` with a password argument, which can be customized. + +```{r better-key-set} +#| eval: false +# takes the arguments `service`, `username` and `keyring` +key_set <- function(...) { + # check whether this is executed in interactive mode + if (interactive()) { + keyring::key_set(...) + } else { + # when run as a script, getPass will wait for user input + keyring::key_set_with_value( + ..., + password = getPass::getPass("Please enter your secret:") + ) + } +} + +# example usage +key_set( + service = "diary", + username = "mickey", + keyring = "vault" +) + +# print(key_get("diary", "mickey", keyring = "vault")) +``` + + +This could be solved with `readLines("stdin", n = 1)`, yet then the user input is visible in plain text during typing. +The terminal solution I found is the R library `getPass`; however, you could also search for a dialog box option to enter the password, or a confirmation mechanism. + + +Another option would be to use [`configr`](https://github.com/Miachol/configr) and store a config file with secrets in a system vault (e.g. with ["tomb"](https://dyne.org/tomb/)). + +Get creative. + + +### Locking and Unlocking + +You might notice that, once your keyring is created, the master password is never asked again. +*What good is a vault for if you do not lock the door?* + + +At the end of your script, or even better: after finishing an operation, you might want to lock your keyring. + +```{r locking-and-unlocking} +#| eval: false + +keyring::keyring_lock(keyring = "vault") +# keyring::keyring_unlock(keyring = "vault") +``` + +This is critical: +Because you work on the **system keyring**, your keyring stays open even after the R sessions closed. + +Normally, it should be locked upon reboot. +However, it is good practice to lock your keyrings whenever you procedure using it has finished. + +```{=markdown} +{{% callout note %}} +``` +**Always** make sure that your keyring is locked when the work is finished. + +```{=markdown} +{{% /callout %}} +``` + + +It might be even better practice to already schedule the closing of your keyring at the moment you open it. +Just so you don't forget to leave your door open when you leave. + +```{r locking-unlock} +#| eval: false + +keyring_unlock <- function(keyring = NULL, ...) { + + # unlock the keyring + keyring::keyring_unlock(keyring = keyring, ...) + + # plan a procedure to lock it when the R session ends + # https://stackoverflow.com/a/41179916 + reg.finalizer( + .GlobalEnv, + function(e) { + keyring::keyring_lock(keyring = keyring) + message(glue::glue("Keyring `{keyring}` locked.")) + }, + onexit = TRUE # I wonder whether you could also set a timer... + ) + + return(invisible(NULL)) +} + +keyring_unlock(keyring = "vault") + +``` + + +# Cleanup + +After using this for a while, you might begin to hear a sort of metallic rattling noise whenever you start moving. +These are all the keys you are bringing along on your dear old `keyring`, but forgot about. + + +Help is near: + +```{r list-keys} +#| eval: false +key_list() +key_list(keyring = "vault") + +``` + + +To delete the naïve key we created above: + +```{r delete-keys} +#| eval: false +key_delete("diary-entry-1") + +``` + + +And, for even more paranoia, delete your entire keyring: + +```{r delete-keyring} +#| eval: false +keyring_delete("vault") +``` + +This will prompt you if the keyring is not empty, so in a scripted situation, you would want to delete all keys prior to deleting the empty keyring. + + +# Summary + +Never ever fall to the temptation of hardcoding a password, anywhere. +It almost inevitably will cause you trouble later on. + + +The longer-than-intended text above captures my own musings with the great `keyring` package for R. +I sincerely hope that it has made your code safer. +I certainly would have loved to know about all those things back then when puberty hit me. + + +# TL;DR + +For the young and impatient, here are all the tricks from above in one code block. + +```{r summary} +library("keyring") +library("getPass") + + +keyring_create <- function(...) { + suppressWarnings( + keyring::keyring_create(..., password = "") + ) +} + +# reduce the chance of accidental printout of a key +key_get <- function(...) invisible(keyring::key_get(...)) + +# setting keys in a scripted environment +# takes the arguments `service`, `username` and `keyring` +key_set <- function(...) { + # check whether this is executed in interactive mode + if (interactive()) { + keyring::key_set(...) + } else { + # when run as a script, getPass will wait for user input + keyring::key_set_with_value( + ..., + password = getPass::getPass("Please enter your secret:") + ) + } +} + + +# unlock a keyring, and schedule locking for when the R session ends +keyring_unlock <- function(keyring = NULL, ...) { + + # unlock the keyring + keyring::keyring_unlock(keyring = keyring, ...) + + # plan a procedure to lock it when the R session ends + # https://stackoverflow.com/a/41179916 + reg.finalizer( + .GlobalEnv, + function(e) { + keyring::keyring_lock(keyring = keyring) + message(glue::glue("Keyring `{keyring}` locked.")) + }, + onexit = TRUE # I wonder whether you could also set a timer... + ) + + return(invisible(NULL)) +} + +``` + +- Never hard-code or store plain text secrets. +- Always create a custom keyring. +- Lock your keyring from within each process which uses it. +- Inspect and clean up you keyrings regularly. + + +*Stay safe, everyone!* diff --git a/content/tutorials/r_keyring/research-institute-for-nature-and-forest.csl b/content/tutorials/r_keyring/research-institute-for-nature-and-forest.csl new file mode 100644 index 000000000..bad203bb5 --- /dev/null +++ b/content/tutorials/r_keyring/research-institute-for-nature-and-forest.csl @@ -0,0 +1,298 @@ + + From b91b6605c50de920c3760ee5e159ac4f26ea57c6 Mon Sep 17 00:00:00 2001 From: Falk Mielke Date: Fri, 5 Sep 2025 13:13:33 +0200 Subject: [PATCH 02/11] r_keyring: proofread; formatting lists --- content/tutorials/r_keyring/index.md | 74 +++++++++------ .../tutorials/r_keyring/keyring_tutorial.qmd | 94 +++++++++++-------- 2 files changed, 101 insertions(+), 67 deletions(-) diff --git a/content/tutorials/r_keyring/index.md b/content/tutorials/r_keyring/index.md index 7117bb6e9..a9f6b3468 100644 --- a/content/tutorials/r_keyring/index.md +++ b/content/tutorials/r_keyring/index.md @@ -88,18 +88,18 @@ keyring::key_get("diary-entry-1") And, admitted, I used keyring like so for quite a while now. However, then I noticed a serious problem, which is there by design: -{{% callout warning %}} +{{% callout note %}} -- The default keyring is not password-locked. -- The secrets persist across R sessions, potentially even across reboots. +- The default keyring is not password-locked: it gets unlocked at user login. +- The system secrets persist across R sessions, potentially even across reboots. {{% /callout %}} -This is an unfortunate combination: it means that if you use `key_set`, and enter your password, you are in a worse situation than before. -You just created a diary without a lock and left it on your desk, so that everyone who likes can just enter your room and read that good-looking Mickey gave you a complement about your new braces :blush:. +This is an unfortunate combination: it means that if you used `key_set`, and entered your password, you are in a worse situation than before. +You just created a diary without a lock and left it on your desk, so that everyone who likes can just enter your room and read that good-looking Mickey gave you a compliment about your new braces :blush:. -In computer terms: anyone who comes to your computer can access the password in a new R session with `key_get`. -If I am not mistaken, passwords in some keyrings even persist reboots, yet I did not test the default one. +In computer terms: anyone who comes to your computer can access the recent passwords in a new R session with `key_get`. +If I am not mistaken, passwords in some keyrings even persist reboots, yet I did not test. What a lousy vault. Laxity killed secrecy. @@ -113,7 +113,7 @@ And this is actually what the authors of `keyring` correctly explain [in the "Us keyring_create(keyring = "vault") ``` -On my computer, in a fresh R terminal, this initiated three things which happen in a row. +On my computer, in a fresh R terminal, this single command initiated three things which happen in a row. - 1. Asks the user for a password on the terminal. - 1. Opens a (very neat) popup window which asks for password and confirmation, indicating password strength. @@ -123,14 +123,24 @@ This might be confusing, and three passwords seem one too much. I guess this is historic burden, or compatibility for headless systems, and found that in my workflow I can safely get rid of points (i) and (iii) by instead using the following: ``` r -suppressWarnings(keyring::keyring_create(keyring = "vault", password = "")) +suppressWarnings(keyring_create(keyring = "vault", password = "")) ``` The first password is given as empty string, and thus not prompted, and the ignorant warning message gets suppressed. -This is the keyring which we can use to store our secrets. +It is possible to wrap this in a function and shadow the package function: + +``` r +keyring_create <- function(...) { + suppressWarnings( + keyring::keyring_create(..., password = "") + ) +} +``` + +Here you go, a personal keyring which we can use to store secrets. It is locked and unlocked with a master password, and any secrets stored inside will be encrypted. -Keep in mind that your keyring will survive a reboot, unless you delete it. +Keep in mind that your keyring will not lock automatically and will survive a reboot, unless you delete it. More on all that, below. @@ -154,7 +164,7 @@ Here, I named it `diary`, for the sake of analogy. Then, there is the `username`, which makes sense in a service created for storing username-password credentials. You ask the "diary"-service of your "vault"-keyring for the password of/to "mickey". -Generally, think of it as a label, or a secret message[^1], which you provide to your vault when you want to retrieve your secret... +Generally, think of it as a label, or a secret phrase[^1] which you provide to your vault when you want to retrieve your secret... ## Recovering Secrets @@ -166,11 +176,13 @@ And that would work like so: key_get("diary", "mickey", keyring = "vault") ``` + [1] "Python is better than R!" + Wait... OMG... -Did my R console just print the secret we entered earlier?! +Why did my R console just print that top secret passphrase I entered earlier?! ### Invisible Getting @@ -183,7 +195,7 @@ key_get <- function(...) invisible(keyring::key_get(...)) *Welcome to next level paranoia.* However, this indicates what I think `keyring` is actually made for: -you actually want to assign your secrets to some variable, temporarily, as in +you actually want to assign your secrets to some variable, temporarily and on-the-fly, as in ``` r database_connection <- DBI::dbConnect( @@ -214,12 +226,12 @@ What does *(non-)interactive* mean? Say you have an automated process: a script which will write all the secret events of the day into your diary. Or a script which performs data processes on an SQL database. -The script is called `secrecy_processing.R`. +The script is called `secrecy_processing.R`, contains a call to `key_set`, and is run prior to executing other scripts. -The working of `key_set(...)` depends on whether you run the code inside that script from a system terminal, like in `Rscript secrecy_processing.R`, or from the R console or another script, e.g. `source('secrecy_processing.R')`. -The second situation, *interactive mode*, is equivalend to what RStudio does, and works conveniently for most people. +The working of `key_set(...)` in there depends on whether you run the code inside that script from a system terminal, like in `Rscript secrecy_processing.R`, or from the R console or another script, e.g. `source('secrecy_processing.R')`. +The second situation, *interactive mode*, is equivalend to what RStudio does, and works conveniently for most users. -Yet in case you have an automated process, one that you run from a terminal or via a `cronjob`, `key_set` fails because it takes continuous input: it will just use the next line that it receives and think it is the password. +Yet in case you have an automated `isFALSE(interactive())` process, one that you run from a terminal or via a `cronjob` (exactly where `keyring` shines), `key_set` fails because it takes continuous input: it will just use the next line that it receives and think it is the password. This is a weird quirk[^2] which has caused me some headache, but at least I [learned about R's `interactive` mode](https://stackoverflow.com/a/27114322) on the way. @@ -251,7 +263,8 @@ key_set( ``` This could be solved with `readLines("stdin", n = 1)`, yet then the user input is visible in plain text during typing. -The terminal solution I found is the R library `getPass`; however, you could also search for a dialog box option to enter the password, or a confirmation mechanism. +The next best terminal solution I found is the R library `getPass`. +You could also search for a dialog box option to enter the password, or a confirmation mechanism. Another option would be to use [`configr`](https://github.com/Miachol/configr) and store a config file with secrets in a system vault (e.g. with ["tomb"](https://dyne.org/tomb/)). @@ -259,21 +272,21 @@ Get creative. ### Locking and Unlocking -You might notice that, once your keyring is created, the master password is never asked again. +You might notice that, once your keyring is created, the master password is rarely asked again. *What good is a vault for if you do not lock the door?* At the end of your script, or even better: after finishing an operation, you might want to lock your keyring. ``` r -keyring::keyring_lock(keyring = "vault") -# keyring::keyring_unlock(keyring = "vault") +keyring_lock(keyring = "vault") +# keyring_unlock(keyring = "vault") ``` This is critical: Because you work on the **system keyring**, your keyring stays open even after the R sessions closed. Normally, it should be locked upon reboot. -However, it is good practice to lock your keyrings whenever you procedure using it has finished. +However, it is good practice to "consciously" lock your keyrings whenever you procedure using it has finished. {{% callout note %}} @@ -307,12 +320,12 @@ keyring_unlock <- function(keyring = NULL, ...) { keyring_unlock(keyring = "vault") ``` -# Cleanup +(Requires an explicit `keyring_unlock` prior to any `key_set` or `key_get` operation.) -After using this for a while, you might begin to hear a sort of metallic rattling noise whenever you start moving. -These are all the keys you are bringing along on your dear old `keyring`, but forgot about. +# Cleanup -Help is near: +After using all this for a while, you might begin to hear a sort of metallic rattling noise whenever you start moving. +These are all the keys on your dear old `keyring` which you are bringing along but forgot about. ``` r key_list() @@ -325,7 +338,7 @@ To delete the naïve key we created above: key_delete("diary-entry-1") ``` -And, for even more paranoia, delete your entire keyring: +And, for even more paranoia and tracelessness, delete your entire keyring: ``` r keyring_delete("vault") @@ -338,7 +351,7 @@ This will prompt you if the keyring is not empty, so in a scripted situation, yo Never ever fall to the temptation of hardcoding a password, anywhere. It almost inevitably will cause you trouble later on. -The longer-than-intended text above captures my own musings with the great `keyring` package for R. +The story above captures my own musings with the great `keyring` package for R. I sincerely hope that it has made your code safer. I certainly would have loved to know about all those things back then when puberty hit me. @@ -401,9 +414,10 @@ keyring_unlock <- function(keyring = NULL, ...) { - Always create a custom keyring. - Lock your keyring from within each process which uses it. - Inspect and clean up you keyrings regularly. +- R I/O-functions can behave differently in `interactive()` mode. *Stay safe, everyone!* [^1]: This `username` itself is subject to secrecy, in a sense that you might increase security in certain situations by not hardcoding it (you could ask the user "which secret would you like to retrieve", with another `getPass`, stay tuned to see how that works). -[^2]: Note that scripted `key_set` is probably not in the intention of the creators of `keyring`, relying on system keyring persistence across sessions; yet I for my part like to delete all secrets from memory after a process has finished. +[^2]: Note that scripted `key_set` is probably not in the intention of the creators of `keyring`, relying on system keyring persistence across sessions; yet I for my part prefer to delete all secrets from memory and clean up each time a process has finished. diff --git a/content/tutorials/r_keyring/keyring_tutorial.qmd b/content/tutorials/r_keyring/keyring_tutorial.qmd index 274c89921..4975a31f3 100644 --- a/content/tutorials/r_keyring/keyring_tutorial.qmd +++ b/content/tutorials/r_keyring/keyring_tutorial.qmd @@ -63,7 +63,7 @@ best time to keep them private. But to sort these secret new emotions, it would be good to put them to paper. **Let us see what options you have to store our secret thoughts.** -- (1) You could just write them on a *plain paper* on your desk. +- 1) You could just write them on a *plain paper* on your desk. Good that they are sorted. Bad that anyone can read them: mum, pa, and that annoying little brother. @@ -71,13 +71,13 @@ Bad that anyone can read them: mum, pa, and that annoying little brother. This is what we call *"hardcoded, unencrypted"* storage, and it is precicely what needs to be avoided. -- (2) You can write them in a *diary with a lock*. +- 2) You can write them in a *diary with a lock*. Better. Yet if someone has the key, or can xray-view the text in the diary, your secrets are still exposed. -- (3) You can write in a secret language (*encryption*), in a locked diary. +- 3) You can write in a secret language (*encryption*), in a locked diary. It turns out that your computer already has such a diary place, a safe space where it can store secrets and credentials. @@ -124,21 +124,22 @@ However, then I noticed a serious problem, which is there by design: ```{=markdown} -{{% callout warning %}} +{{% callout note %}} ``` -- The default keyring is not password-locked. -- The secrets persist across R sessions, potentially even across reboots. +- The default keyring is not password-locked: it gets unlocked at user login. +- The system secrets persist across R sessions, potentially even across reboots. ```{=markdown} {{% /callout %}} ``` -This is an unfortunate combination: it means that if you use `key_set`, and enter your password, you are in a worse situation than before. -You just created a diary without a lock and left it on your desk, so that everyone who likes can just enter your room and read that good-looking Mickey gave you a complement about your new braces :blush:. + +This is an unfortunate combination: it means that if you used `key_set`, and entered your password, you are in a worse situation than before. +You just created a diary without a lock and left it on your desk, so that everyone who likes can just enter your room and read that good-looking Mickey gave you a compliment about your new braces :blush:. -In computer terms: anyone who comes to your computer can access the password in a new R session with `key_get`. -If I am not mistaken, passwords in some keyrings even persist reboots, yet I did not test the default one. +In computer terms: anyone who comes to your computer can access the recent passwords in a new R session with `key_get`. +If I am not mistaken, passwords in some keyrings even persist reboots, yet I did not test. What a lousy vault. @@ -155,11 +156,11 @@ And this is actually what the authors of `keyring` correctly explain [in the "Us keyring_create(keyring = "vault") ``` -On my computer, in a fresh R terminal, this initiated three things which happen in a row. +On my computer, in a fresh R terminal, this single command initiated three things which happen in a row. -- (i) Asks the user for a password on the terminal. -- (ii) Opens a (very neat) popup window which asks for password and confirmation, indicating password strength. -- (iii) Issues a warning that `Password ignored, will be read interactively`. +- i) Asks the user for a password on the terminal. +- ii) Opens a (very neat) popup window which asks for password and confirmation, indicating password strength. +- iii) Issues a warning that `Password ignored, will be read interactively`. This might be confusing, and three passwords seem one too much. @@ -167,18 +168,30 @@ I guess this is historic burden, or compatibility for headless systems, and foun ```{r better-create-keyring} #| eval: false -suppressWarnings(keyring::keyring_create(keyring = "vault", password = "")) +suppressWarnings(keyring_create(keyring = "vault", password = "")) ``` The first password is given as empty string, and thus not prompted, and the ignorant warning message gets suppressed. -This is the keyring which we can use to store our secrets. +It is possible to wrap this in a function and shadow the package function: + +```{r always-better-create-keyring} +#| eval: false +keyring_create <- function(...) { + suppressWarnings( + keyring::keyring_create(..., password = "") + ) +} +``` + + +Here you go, a personal keyring which we can use to store secrets. It is locked and unlocked with a master password, and any secrets stored inside will be encrypted. -Keep in mind that your keyring will survive a reboot, unless you delete it. +Keep in mind that your keyring will not lock automatically and will survive a reboot, unless you delete it. -More on all that, below. +More on all that, below. # Managing Keys @@ -204,7 +217,7 @@ Here, I named it `diary`, for the sake of analogy. Then, there is the `username`, which makes sense in a service created for storing username-password credentials. You ask the "diary"-service of your "vault"-keyring for the password of/to "mickey". -Generally, think of it as a label, or a secret message[^1], which you provide to your vault when you want to retrieve your secret... +Generally, think of it as a label, or a secret phrase[^1] which you provide to your vault when you want to retrieve your secret... [^1]: This `username` itself is subject to secrecy, in a sense that you might increase security in certain situations by not hardcoding it (you could ask the user "which secret would you like to retrieve", with another `getPass`, stay tuned to see how that works). @@ -220,11 +233,16 @@ And that would work like so: key_get("diary", "mickey", keyring = "vault") ``` +``` +[1] "Python is better than R!" +``` + + Wait... OMG... -Did my R console just print the secret we entered earlier?! +Why did my R console just print that top secret passphrase I entered earlier?! ### Invisible Getting @@ -239,7 +257,7 @@ key_get <- function(...) invisible(keyring::key_get(...)) However, this indicates what I think `keyring` is actually made for: -you actually want to assign your secrets to some variable, temporarily, as in +you actually want to assign your secrets to some variable, temporarily and on-the-fly, as in ```{r scripted-use} #| eval: false @@ -278,18 +296,18 @@ What does *(non-)interactive* mean? Say you have an automated process: a script which will write all the secret events of the day into your diary. Or a script which performs data processes on an SQL database. -The script is called `secrecy_processing.R`. +The script is called `secrecy_processing.R`, contains a call to `key_set`, and is run prior to executing other scripts. -The working of `key_set(...)` depends on whether you run the code inside that script from a system terminal, like in `Rscript secrecy_processing.R`, or from the R console or another script, e.g. `source('secrecy_processing.R')`. -The second situation, *interactive mode*, is equivalend to what RStudio does, and works conveniently for most people. +The working of `key_set(...)` in there depends on whether you run the code inside that script from a system terminal, like in `Rscript secrecy_processing.R`, or from the R console or another script, e.g. `source('secrecy_processing.R')`. +The second situation, *interactive mode*, is equivalend to what RStudio does, and works conveniently for most users. -Yet in case you have an automated process, one that you run from a terminal or via a `cronjob`, `key_set` fails because it takes continuous input: it will just use the next line that it receives and think it is the password. +Yet in case you have an automated `isFALSE(interactive())` process, one that you run from a terminal or via a `cronjob` (exactly where `keyring` shines), `key_set` fails because it takes continuous input: it will just use the next line that it receives and think it is the password. This is a weird quirk[^2] which has caused me some headache, but at least I [learned about R's `interactive` mode](https://stackoverflow.com/a/27114322) on the way. -[^2]: Note that scripted `key_set` is probably not in the intention of the creators of `keyring`, relying on system keyring persistence across sessions; yet I for my part like to delete all secrets from memory after a process has finished. +[^2]: Note that scripted `key_set` is probably not in the intention of the creators of `keyring`, relying on system keyring persistence across sessions; yet I for my part prefer to delete all secrets from memory and clean up each time a process has finished. Here is the workaround: `keyring` brings a function to `key_set_with_value` with a password argument, which can be customized. @@ -322,7 +340,8 @@ key_set( This could be solved with `readLines("stdin", n = 1)`, yet then the user input is visible in plain text during typing. -The terminal solution I found is the R library `getPass`; however, you could also search for a dialog box option to enter the password, or a confirmation mechanism. +The next best terminal solution I found is the R library `getPass`. +You could also search for a dialog box option to enter the password, or a confirmation mechanism. Another option would be to use [`configr`](https://github.com/Miachol/configr) and store a config file with secrets in a system vault (e.g. with ["tomb"](https://dyne.org/tomb/)). @@ -332,7 +351,7 @@ Get creative. ### Locking and Unlocking -You might notice that, once your keyring is created, the master password is never asked again. +You might notice that, once your keyring is created, the master password is rarely asked again. *What good is a vault for if you do not lock the door?* @@ -341,15 +360,15 @@ At the end of your script, or even better: after finishing an operation, you mig ```{r locking-and-unlocking} #| eval: false -keyring::keyring_lock(keyring = "vault") -# keyring::keyring_unlock(keyring = "vault") +keyring_lock(keyring = "vault") +# keyring_unlock(keyring = "vault") ``` This is critical: Because you work on the **system keyring**, your keyring stays open even after the R sessions closed. Normally, it should be locked upon reboot. -However, it is good practice to lock your keyrings whenever you procedure using it has finished. +However, it is good practice to "consciously" lock your keyrings whenever you procedure using it has finished. ```{=markdown} {{% callout note %}} @@ -390,14 +409,14 @@ keyring_unlock(keyring = "vault") ``` +(Requires an explicit `keyring_unlock` prior to any `key_set` or `key_get` operation.) -# Cleanup -After using this for a while, you might begin to hear a sort of metallic rattling noise whenever you start moving. -These are all the keys you are bringing along on your dear old `keyring`, but forgot about. +# Cleanup +After using all this for a while, you might begin to hear a sort of metallic rattling noise whenever you start moving. +These are all the keys on your dear old `keyring` which you are bringing along but forgot about. -Help is near: ```{r list-keys} #| eval: false @@ -416,7 +435,7 @@ key_delete("diary-entry-1") ``` -And, for even more paranoia, delete your entire keyring: +And, for even more paranoia and tracelessness, delete your entire keyring: ```{r delete-keyring} #| eval: false @@ -432,7 +451,7 @@ Never ever fall to the temptation of hardcoding a password, anywhere. It almost inevitably will cause you trouble later on. -The longer-than-intended text above captures my own musings with the great `keyring` package for R. +The story above captures my own musings with the great `keyring` package for R. I sincerely hope that it has made your code safer. I certainly would have loved to know about all those things back then when puberty hit me. @@ -497,6 +516,7 @@ keyring_unlock <- function(keyring = NULL, ...) { - Always create a custom keyring. - Lock your keyring from within each process which uses it. - Inspect and clean up you keyrings regularly. +- R I/O-functions can behave differently in `interactive()` mode. *Stay safe, everyone!* From 0f04d9690eecffa6b6cd76d377109433850eedc0 Mon Sep 17 00:00:00 2001 From: Falk Mielke Date: Fri, 5 Sep 2025 13:17:21 +0200 Subject: [PATCH 03/11] r_keyring: lists attempt 2 --- content/tutorials/r_keyring/index.md | 14 ++++++-------- content/tutorials/r_keyring/keyring_tutorial.qmd | 12 ++++++------ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/content/tutorials/r_keyring/index.md b/content/tutorials/r_keyring/index.md index a9f6b3468..298e080ff 100644 --- a/content/tutorials/r_keyring/index.md +++ b/content/tutorials/r_keyring/index.md @@ -1,5 +1,3 @@ - - --- title: "The `keyring` package: We can do better than `*******`." description: "Using your system keyring for storing and accessing secrets from within R via the `keyring` package" @@ -38,19 +36,19 @@ best time to keep them private. But to sort these secret new emotions, it would be good to put them to paper. **Let us see what options you have to store our secret thoughts.** -- 1. You could just write them on a *plain paper* on your desk. +- 1\. You could just write them on a *plain paper* on your desk. Good that they are sorted. Bad that anyone can read them: mum, pa, and that annoying little brother. This is what we call *"hardcoded, unencrypted"* storage, and it is precicely what needs to be avoided. -- 1. You can write them in a *diary with a lock*. +- 2\. You can write them in a *diary with a lock*. Better. Yet if someone has the key, or can xray-view the text in the diary, your secrets are still exposed. -- 1. You can write in a secret language (*encryption*), in a locked diary. +- 3\. You can write in a secret language (*encryption*), in a locked diary. It turns out that your computer already has such a diary place, a safe space where it can store secrets and credentials. It is called the **"system keyring"**. @@ -115,9 +113,9 @@ keyring_create(keyring = "vault") On my computer, in a fresh R terminal, this single command initiated three things which happen in a row. -- 1. Asks the user for a password on the terminal. -- 1. Opens a (very neat) popup window which asks for password and confirmation, indicating password strength. -- 1. Issues a warning that `Password ignored, will be read interactively`. +- i\. Asks the user for a password on the terminal. +- ii\. Opens a (very neat) popup window which asks for password and confirmation, indicating password strength. +- iii\. Issues a warning that `Password ignored, will be read interactively`. This might be confusing, and three passwords seem one too much. I guess this is historic burden, or compatibility for headless systems, and found that in my workflow I can safely get rid of points (i) and (iii) by instead using the following: diff --git a/content/tutorials/r_keyring/keyring_tutorial.qmd b/content/tutorials/r_keyring/keyring_tutorial.qmd index 4975a31f3..b1b211cb2 100644 --- a/content/tutorials/r_keyring/keyring_tutorial.qmd +++ b/content/tutorials/r_keyring/keyring_tutorial.qmd @@ -63,7 +63,7 @@ best time to keep them private. But to sort these secret new emotions, it would be good to put them to paper. **Let us see what options you have to store our secret thoughts.** -- 1) You could just write them on a *plain paper* on your desk. +- 1\. You could just write them on a *plain paper* on your desk. Good that they are sorted. Bad that anyone can read them: mum, pa, and that annoying little brother. @@ -71,13 +71,13 @@ Bad that anyone can read them: mum, pa, and that annoying little brother. This is what we call *"hardcoded, unencrypted"* storage, and it is precicely what needs to be avoided. -- 2) You can write them in a *diary with a lock*. +- 2\. You can write them in a *diary with a lock*. Better. Yet if someone has the key, or can xray-view the text in the diary, your secrets are still exposed. -- 3) You can write in a secret language (*encryption*), in a locked diary. +- 3\. You can write in a secret language (*encryption*), in a locked diary. It turns out that your computer already has such a diary place, a safe space where it can store secrets and credentials. @@ -158,9 +158,9 @@ keyring_create(keyring = "vault") On my computer, in a fresh R terminal, this single command initiated three things which happen in a row. -- i) Asks the user for a password on the terminal. -- ii) Opens a (very neat) popup window which asks for password and confirmation, indicating password strength. -- iii) Issues a warning that `Password ignored, will be read interactively`. +- i\. Asks the user for a password on the terminal. +- ii\. Opens a (very neat) popup window which asks for password and confirmation, indicating password strength. +- iii\. Issues a warning that `Password ignored, will be read interactively`. This might be confusing, and three passwords seem one too much. From 7fb7682e8cf57011bb650d38f8a8ee243072916a Mon Sep 17 00:00:00 2001 From: Falk Mielke Date: Fri, 5 Sep 2025 13:24:49 +0200 Subject: [PATCH 04/11] r_keyring: lists attempt 3 and 4 --- content/tutorials/r_keyring/index.md | 22 ++++++++++++++----- .../tutorials/r_keyring/keyring_tutorial.qmd | 15 +++++++------ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/content/tutorials/r_keyring/index.md b/content/tutorials/r_keyring/index.md index 298e080ff..91e881b62 100644 --- a/content/tutorials/r_keyring/index.md +++ b/content/tutorials/r_keyring/index.md @@ -1,3 +1,5 @@ + + --- title: "The `keyring` package: We can do better than `*******`." description: "Using your system keyring for storing and accessing secrets from within R via the `keyring` package" @@ -36,19 +38,19 @@ best time to keep them private. But to sort these secret new emotions, it would be good to put them to paper. **Let us see what options you have to store our secret thoughts.** -- 1\. You could just write them on a *plain paper* on your desk. +1\. You could just write them on a *plain paper* on your desk. Good that they are sorted. Bad that anyone can read them: mum, pa, and that annoying little brother. This is what we call *"hardcoded, unencrypted"* storage, and it is precicely what needs to be avoided. -- 2\. You can write them in a *diary with a lock*. +2\. You can write them in a *diary with a lock*. Better. Yet if someone has the key, or can xray-view the text in the diary, your secrets are still exposed. -- 3\. You can write in a secret language (*encryption*), in a locked diary. +3\. You can write in a secret language (*encryption*), in a locked diary. It turns out that your computer already has such a diary place, a safe space where it can store secrets and credentials. It is called the **"system keyring"**. @@ -113,9 +115,17 @@ keyring_create(keyring = "vault") On my computer, in a fresh R terminal, this single command initiated three things which happen in a row. -- i\. Asks the user for a password on the terminal. -- ii\. Opens a (very neat) popup window which asks for password and confirmation, indicating password strength. -- iii\. Issues a warning that `Password ignored, will be read interactively`. +
    +
  1. +Asks the user for a password on the terminal. +
  2. +
  3. +Opens a (very neat) popup window which asks for password and confirmation, indicating password strength. +
  4. +
  5. +Issues a warning that `Password ignored, will be read interactively`. +
  6. +
This might be confusing, and three passwords seem one too much. I guess this is historic burden, or compatibility for headless systems, and found that in my workflow I can safely get rid of points (i) and (iii) by instead using the following: diff --git a/content/tutorials/r_keyring/keyring_tutorial.qmd b/content/tutorials/r_keyring/keyring_tutorial.qmd index b1b211cb2..fb5da27d4 100644 --- a/content/tutorials/r_keyring/keyring_tutorial.qmd +++ b/content/tutorials/r_keyring/keyring_tutorial.qmd @@ -63,7 +63,7 @@ best time to keep them private. But to sort these secret new emotions, it would be good to put them to paper. **Let us see what options you have to store our secret thoughts.** -- 1\. You could just write them on a *plain paper* on your desk. +1\. You could just write them on a *plain paper* on your desk. Good that they are sorted. Bad that anyone can read them: mum, pa, and that annoying little brother. @@ -71,13 +71,13 @@ Bad that anyone can read them: mum, pa, and that annoying little brother. This is what we call *"hardcoded, unencrypted"* storage, and it is precicely what needs to be avoided. -- 2\. You can write them in a *diary with a lock*. +2\. You can write them in a *diary with a lock*. Better. Yet if someone has the key, or can xray-view the text in the diary, your secrets are still exposed. -- 3\. You can write in a secret language (*encryption*), in a locked diary. +3\. You can write in a secret language (*encryption*), in a locked diary. It turns out that your computer already has such a diary place, a safe space where it can store secrets and credentials. @@ -158,10 +158,11 @@ keyring_create(keyring = "vault") On my computer, in a fresh R terminal, this single command initiated three things which happen in a row. -- i\. Asks the user for a password on the terminal. -- ii\. Opens a (very neat) popup window which asks for password and confirmation, indicating password strength. -- iii\. Issues a warning that `Password ignored, will be read interactively`. - +
    +
  1. Asks the user for a password on the terminal.
  2. +
  3. Opens a (very neat) popup window which asks for password and confirmation, indicating password strength.
  4. +
  5. Issues a warning that `Password ignored, will be read interactively`.
  6. +
This might be confusing, and three passwords seem one too much. I guess this is historic burden, or compatibility for headless systems, and found that in my workflow I can safely get rid of points (i) and (iii) by instead using the following: From 46c74c49d15d2172fd7c3993d8e2c71b44d74be1 Mon Sep 17 00:00:00 2001 From: Falk Mielke Date: Fri, 5 Sep 2025 13:28:17 +0200 Subject: [PATCH 05/11] r_keyring: lists attempt 6 --- content/tutorials/r_keyring/index.md | 18 +++++------------- .../tutorials/r_keyring/keyring_tutorial.qmd | 11 ++++++----- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/content/tutorials/r_keyring/index.md b/content/tutorials/r_keyring/index.md index 91e881b62..a82a357b9 100644 --- a/content/tutorials/r_keyring/index.md +++ b/content/tutorials/r_keyring/index.md @@ -1,5 +1,3 @@ - - --- title: "The `keyring` package: We can do better than `*******`." description: "Using your system keyring for storing and accessing secrets from within R via the `keyring` package" @@ -115,17 +113,11 @@ keyring_create(keyring = "vault") On my computer, in a fresh R terminal, this single command initiated three things which happen in a row. -
    -
  1. -Asks the user for a password on the terminal. -
  2. -
  3. -Opens a (very neat) popup window which asks for password and confirmation, indicating password strength. -
  4. -
  5. -Issues a warning that `Password ignored, will be read interactively`. -
  6. -
+... i. Asks the user for a password on the terminal. + +... ii. Opens a (very neat) popup window which asks for password and confirmation, indicating password strength. + +... iii. Issues a warning that `Password ignored, will be read interactively`. This might be confusing, and three passwords seem one too much. I guess this is historic burden, or compatibility for headless systems, and found that in my workflow I can safely get rid of points (i) and (iii) by instead using the following: diff --git a/content/tutorials/r_keyring/keyring_tutorial.qmd b/content/tutorials/r_keyring/keyring_tutorial.qmd index fb5da27d4..e68e6a12e 100644 --- a/content/tutorials/r_keyring/keyring_tutorial.qmd +++ b/content/tutorials/r_keyring/keyring_tutorial.qmd @@ -158,11 +158,12 @@ keyring_create(keyring = "vault") On my computer, in a fresh R terminal, this single command initiated three things which happen in a row. -
    -
  1. Asks the user for a password on the terminal.
  2. -
  3. Opens a (very neat) popup window which asks for password and confirmation, indicating password strength.
  4. -
  5. Issues a warning that `Password ignored, will be read interactively`.
  6. -
+... i\. Asks the user for a password on the terminal. + +... ii\. Opens a (very neat) popup window which asks for password and confirmation, indicating password strength. + +... iii\. Issues a warning that `Password ignored, will be read interactively`. + This might be confusing, and three passwords seem one too much. I guess this is historic burden, or compatibility for headless systems, and found that in my workflow I can safely get rid of points (i) and (iii) by instead using the following: From 07f794e35a8ff4c0b86ff9d0553732d2d64febdb Mon Sep 17 00:00:00 2001 From: Falk Mielke Date: Fri, 5 Sep 2025 16:03:05 +0200 Subject: [PATCH 06/11] r_keyring: delayed locking function --- content/tutorials/r_keyring/index.md | 39 ++++++++++++++++++ .../tutorials/r_keyring/keyring_tutorial.qmd | 40 +++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/content/tutorials/r_keyring/index.md b/content/tutorials/r_keyring/index.md index a82a357b9..46be56821 100644 --- a/content/tutorials/r_keyring/index.md +++ b/content/tutorials/r_keyring/index.md @@ -1,3 +1,5 @@ + + --- title: "The `keyring` package: We can do better than `*******`." description: "Using your system keyring for storing and accessing secrets from within R via the `keyring` package" @@ -322,6 +324,43 @@ keyring_unlock(keyring = "vault") (Requires an explicit `keyring_unlock` prior to any `key_set` or `key_get` operation.) +For the Linux operating system, I assembled a function which will spawn a background process for delayed locking of the keyring: + +``` r +# lock the keyring after a delay +lock_keyring_delayed <- function(keyring_label, delay = 300) { + + stopifnot("glue" = require("glue")) + stopifnot("keyring" = require("keyring")) + + # string building blocks + l <- glue::glue('\"{keyring_label}\"') + k <- 'keyring::keyring_' + x <- glue::glue('({l} %in% keyring::keyring_list()$keyring)') + + # this only works on linux + if (isFALSE(.Platform$OS.type == "unix")) { + message(glue::glue(" + (keyring will not lock with delay; + invoke 'keyring::keyring_lock({l})') + ")) + return(invisible(NULL)) + } + + # build the core script + cmd <- glue::glue( + "Rscript -e 'if ({x} && isFALSE({k}is_locked({l}))) {k}lock({l})'" + # && echo 'slam!' # <- for testing + ) + + # background-execute the script with a delay + system(glue::glue("sleep {delay} && {cmd} &", wait = FALSE)) + +} # /lock_keyring_delayed +``` + +However, that still leaves the keyring on the system. + # Cleanup After using all this for a while, you might begin to hear a sort of metallic rattling noise whenever you start moving. diff --git a/content/tutorials/r_keyring/keyring_tutorial.qmd b/content/tutorials/r_keyring/keyring_tutorial.qmd index e68e6a12e..572eb88ef 100644 --- a/content/tutorials/r_keyring/keyring_tutorial.qmd +++ b/content/tutorials/r_keyring/keyring_tutorial.qmd @@ -414,6 +414,46 @@ keyring_unlock(keyring = "vault") (Requires an explicit `keyring_unlock` prior to any `key_set` or `key_get` operation.) +For the Linux operating system, I assembled a function which will spawn a background process for delayed locking of the keyring: + +```{r delayed-locking-linux} +#| eval: false + +# lock the keyring after a delay +lock_keyring_delayed <- function(keyring_label, delay = 300) { + + stopifnot("glue" = require("glue")) + stopifnot("keyring" = require("keyring")) + + # string building blocks + l <- glue::glue('\"{keyring_label}\"') + k <- 'keyring::keyring_' + x <- glue::glue('({l} %in% keyring::keyring_list()$keyring)') + + # this only works on linux + if (isFALSE(.Platform$OS.type == "unix")) { + message(glue::glue(" + (keyring will not lock with delay; + invoke 'keyring::keyring_lock({l})') + ")) + return(invisible(NULL)) + } + + # build the core script + cmd <- glue::glue( + "Rscript -e 'if ({x} && isFALSE({k}is_locked({l}))) {k}lock({l})'" + # && echo 'slam!' # <- for testing + ) + + # background-execute the script with a delay + system(glue::glue("sleep {delay} && {cmd} &", wait = FALSE)) + +} # /lock_keyring_delayed +``` + +However, that still leaves the keyring on the system. + + # Cleanup After using all this for a while, you might begin to hear a sort of metallic rattling noise whenever you start moving. From 230131d22dd09fc7301aaec2459acdc35409ca2e Mon Sep 17 00:00:00 2001 From: Falk Mielke Date: Fri, 5 Sep 2025 16:03:36 +0200 Subject: [PATCH 07/11] r_keyring: empty lines --- content/tutorials/r_keyring/index.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/content/tutorials/r_keyring/index.md b/content/tutorials/r_keyring/index.md index 46be56821..0a78eae73 100644 --- a/content/tutorials/r_keyring/index.md +++ b/content/tutorials/r_keyring/index.md @@ -1,5 +1,3 @@ - - --- title: "The `keyring` package: We can do better than `*******`." description: "Using your system keyring for storing and accessing secrets from within R via the `keyring` package" From 85d251306c224381ad7b212e83b8c2e6e85f1384 Mon Sep 17 00:00:00 2001 From: Falk Mielke Date: Fri, 5 Sep 2025 19:09:16 +0200 Subject: [PATCH 08/11] r_keyring: warning to keep Login keyring --- content/tutorials/r_keyring/index.md | 23 ++++++++++++++++- .../tutorials/r_keyring/keyring_tutorial.qmd | 25 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/content/tutorials/r_keyring/index.md b/content/tutorials/r_keyring/index.md index 0a78eae73..ff3e0225d 100644 --- a/content/tutorials/r_keyring/index.md +++ b/content/tutorials/r_keyring/index.md @@ -176,7 +176,7 @@ And that would work like so: key_get("diary", "mickey", keyring = "vault") ``` - [1] "Python is better than R!" + [1] "I love Python!" Wait... @@ -361,6 +361,8 @@ However, that still leaves the keyring on the system. # Cleanup +## Cleanup Command + After using all this for a while, you might begin to hear a sort of metallic rattling noise whenever you start moving. These are all the keys on your dear old `keyring` which you are bringing along but forgot about. @@ -383,6 +385,22 @@ keyring_delete("vault") This will prompt you if the keyring is not empty, so in a scripted situation, you would want to delete all keys prior to deleting the empty keyring. +## Cleanup System Keyring (Warning!) + +As I experienced, cleanup can go to far. +After finishing this write-up, I cleaned up all the keyrings which had accumulated over time from mis-use of R's `keyring`. +This reminded me that what I demonstrated herein is working on the **host system**, and has implications which go beyond R. + +What went wrong? +There was a keyring I could not place, called `Login`. + +I learned the hard way that the `Login` keyring is a system requirement, and deleting is will cause system issues. +What followed was a desparate, but ultimately succesful attempt to repair arch linux by force-removing the `keyutils` package (which immediately broke the system even more[^3], the package manager depends on it), and then re-install it via sideloading from a live-usb-system. + +Long story short: +**your system keyring is vital**. +Keep it in order. + # Summary Never ever fall to the temptation of hardcoding a password, anywhere. @@ -452,9 +470,12 @@ keyring_unlock <- function(keyring = NULL, ...) { - Lock your keyring from within each process which uses it. - Inspect and clean up you keyrings regularly. - R I/O-functions can behave differently in `interactive()` mode. +- Do not wipe your system keyring, it is an integral component. *Stay safe, everyone!* [^1]: This `username` itself is subject to secrecy, in a sense that you might increase security in certain situations by not hardcoding it (you could ask the user "which secret would you like to retrieve", with another `getPass`, stay tuned to see how that works). [^2]: Note that scripted `key_set` is probably not in the intention of the creators of `keyring`, relying on system keyring persistence across sessions; yet I for my part prefer to delete all secrets from memory and clean up each time a process has finished. + +[^3]: A brick with windows on it, to be exact. diff --git a/content/tutorials/r_keyring/keyring_tutorial.qmd b/content/tutorials/r_keyring/keyring_tutorial.qmd index 572eb88ef..3f62bdbc8 100644 --- a/content/tutorials/r_keyring/keyring_tutorial.qmd +++ b/content/tutorials/r_keyring/keyring_tutorial.qmd @@ -236,7 +236,7 @@ key_get("diary", "mickey", keyring = "vault") ``` ``` -[1] "Python is better than R!" +[1] "I love Python!" ``` @@ -456,6 +456,8 @@ However, that still leaves the keyring on the system. # Cleanup +## Cleanup Command + After using all this for a while, you might begin to hear a sort of metallic rattling noise whenever you start moving. These are all the keys on your dear old `keyring` which you are bringing along but forgot about. @@ -486,6 +488,26 @@ keyring_delete("vault") This will prompt you if the keyring is not empty, so in a scripted situation, you would want to delete all keys prior to deleting the empty keyring. +## Cleanup System Keyring (Warning!) + +As I experienced, cleanup can go to far. +After finishing this write-up, I cleaned up all the keyrings which had accumulated over time from mis-use of R's `keyring`. +This reminded me that what I demonstrated herein is working on the **host system**, and has implications which go beyond R. + + +What went wrong? +There was a keyring I could not place, called `Login`. + +I learned the hard way that the `Login` keyring is a system requirement, and deleting is will cause system issues. +What followed was a desparate, but ultimately succesful attempt to repair arch linux by force-removing the `keyutils` package (which immediately broke the system even more[^3], the package manager depends on it), and then re-install it via sideloading from a live-usb-system. + +[^3]: A brick with windows on it, to be exact. + + +Long story short: +**your system keyring is vital**. +Keep it in order. + # Summary @@ -559,6 +581,7 @@ keyring_unlock <- function(keyring = NULL, ...) { - Lock your keyring from within each process which uses it. - Inspect and clean up you keyrings regularly. - R I/O-functions can behave differently in `interactive()` mode. +- Do not wipe your system keyring, it is an integral component. *Stay safe, everyone!* From 26ee0d05326807671956ef0bee315ca3a6e9e729 Mon Sep 17 00:00:00 2001 From: Falk Mielke <67623545+falkmielke@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:14:28 +0100 Subject: [PATCH 09/11] Update content/tutorials/r_keyring/index.md Co-authored-by: raisa_carmen <56317093+RCinbo@users.noreply.github.com> --- content/tutorials/r_keyring/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/tutorials/r_keyring/index.md b/content/tutorials/r_keyring/index.md index ff3e0225d..42a88999d 100644 --- a/content/tutorials/r_keyring/index.md +++ b/content/tutorials/r_keyring/index.md @@ -286,7 +286,7 @@ This is critical: Because you work on the **system keyring**, your keyring stays open even after the R sessions closed. Normally, it should be locked upon reboot. -However, it is good practice to "consciously" lock your keyrings whenever you procedure using it has finished. +However, it is good practice to "consciously" lock your keyrings whenever your procedure using it has finished. {{% callout note %}} From 3a4c9b1e90960ef041d3ee10e31f10805c8f3eec Mon Sep 17 00:00:00 2001 From: Falk Mielke <67623545+falkmielke@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:14:36 +0100 Subject: [PATCH 10/11] Update content/tutorials/r_keyring/index.md Co-authored-by: raisa_carmen <56317093+RCinbo@users.noreply.github.com> --- content/tutorials/r_keyring/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/tutorials/r_keyring/index.md b/content/tutorials/r_keyring/index.md index 42a88999d..fb1f11b5c 100644 --- a/content/tutorials/r_keyring/index.md +++ b/content/tutorials/r_keyring/index.md @@ -215,7 +215,7 @@ Conclusion: {{% callout note %}} The main use case of `keyring` are scripts and code chains which repeatedly use secrets (because, if it was not repeatedly, `getPass` would be fine). -In these situations, `keyring` helps you to avoid te worse options of (a) hardcoding your secrets or (b) having to type them over and over again. +In these situations, `keyring` helps you to avoid the worse options of (a) hardcoding your secrets or (b) having to type them over and over again. {{% /callout %}} From 3b1b1a3bc688df57cae3dec3762e12c052e06dd3 Mon Sep 17 00:00:00 2001 From: Falk Mielke <67623545+falkmielke@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:14:48 +0100 Subject: [PATCH 11/11] Update content/tutorials/r_keyring/index.md Co-authored-by: raisa_carmen <56317093+RCinbo@users.noreply.github.com> --- content/tutorials/r_keyring/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/tutorials/r_keyring/index.md b/content/tutorials/r_keyring/index.md index fb1f11b5c..98dd3450b 100644 --- a/content/tutorials/r_keyring/index.md +++ b/content/tutorials/r_keyring/index.md @@ -387,7 +387,7 @@ This will prompt you if the keyring is not empty, so in a scripted situation, yo ## Cleanup System Keyring (Warning!) -As I experienced, cleanup can go to far. +As I experienced, cleanup can go too far. After finishing this write-up, I cleaned up all the keyrings which had accumulated over time from mis-use of R's `keyring`. This reminded me that what I demonstrated herein is working on the **host system**, and has implications which go beyond R.