Summary
While reviewing the key-handling code, I noticed that the only key-at-rest mechanism is a plaintext DELPHI_EPIDATA_KEY=... line in .Renviron, that save_api_key() prefers a project-level .Renviron (i.e., a file inside the user's current git repository), and that nothing enforces restrictive file permissions anywhere — the file's mode is whatever the umask produces (typically 0644, world-readable). The package only advises the user to gitignore the project file, and the repo's own .gitignore excludes .env/.secrets/.Rprofile but not .Renviron. I think a few small changes (chmod 0600, confirm before writing a project-level file, a keyring pointer in the docs, and a .gitignore entry) would close the practical exposure.
Evidence (dev tip 87b10e1)
R/auth.R:69-77 — path choice and file creation, no chmod:
# Prefer a project-level .Renviron in the working directory; fall back to
# the user-level one R reads at startup (see ?Startup).
if (file.exists(".Renviron")) {
path <- ".Renviron"
} else {
path <- path.expand(file.path("~", ".Renviron"))
if (!file.exists(path)) {
file.create(path)
}
}
utils::file.edit(path)
On main (09da015) the equivalent is usethis::edit_r_environ(scope = "project") when usethis::proj_path(".Renviron") exists — same project-first preference.
- The project-level advice is the only mitigation (
R/auth.R:79-87): "Make sure not to share this file (add it to .gitignore or equivalents)."
.gitignore (repo root) lists .env, .secrets, .Rprofile, .httr-oauth — but not .Renviron.
- Reader side:
get_api_key() only reads the env var (R/auth.R:37-52); there is no keyring/encrypted-store alternative offered anywhere in the docs.
Maintainer-runnable repro
setwd(tempdir())
file.create(".Renviron")
format(file.info(".Renviron")$mode) # "644" under a default umask of 022 (world-readable)
And the workflow risk: setwd(<a git repo>); save_api_key() opens/creates <repo>/.Renviron — an untracked file one git add -f/git add . away from being committed, with the only protection being the message the user may not read.
Expected vs. actual
- Expected: a credential file is created/edited with owner-only permissions, and storing a key inside a project directory is at least confirmed (and ideally discouraged in favor of the user-level file or a keyring).
- Actual:
0644-mode plaintext key file; project-level placement preferred with only advisory protection; no keyring path documented.
Suggested fix
- After
file.create(path), apply Sys.chmod(path, "0600") (and optionally warn if an existing .Renviron is group/world-readable).
- Before choosing the project-level path, ask for confirmation (and mention the risk that it sits inside a git repository), or prefer the user-level file by default.
- Add
.Renviron to the repo's own .gitignore as an example for contributors.
- Document a keyring-based alternative (e.g. the
keyring package) in ?get_api_key for users who want encrypted storage.
I checked for prior reports (issues #181 "Figure out better API key management", #295, #339 — the last fixed a save_api_key crash, not the storage posture) and found none covering file permissions or the project-file preference.
Summary
While reviewing the key-handling code, I noticed that the only key-at-rest mechanism is a plaintext
DELPHI_EPIDATA_KEY=...line in.Renviron, thatsave_api_key()prefers a project-level.Renviron(i.e., a file inside the user's current git repository), and that nothing enforces restrictive file permissions anywhere — the file's mode is whatever the umask produces (typically0644, world-readable). The package only advises the user to gitignore the project file, and the repo's own.gitignoreexcludes.env/.secrets/.Rprofilebut not.Renviron. I think a few small changes (chmod0600, confirm before writing a project-level file, a keyring pointer in the docs, and a.gitignoreentry) would close the practical exposure.Evidence (dev tip 87b10e1)
R/auth.R:69-77— path choice and file creation, no chmod:main(09da015) the equivalent isusethis::edit_r_environ(scope = "project")whenusethis::proj_path(".Renviron")exists — same project-first preference.R/auth.R:79-87): "Make sure not to share this file (add it to .gitignore or equivalents).".gitignore(repo root) lists.env,.secrets,.Rprofile,.httr-oauth— but not.Renviron.get_api_key()only reads the env var (R/auth.R:37-52); there is no keyring/encrypted-store alternative offered anywhere in the docs.Maintainer-runnable repro
And the workflow risk:
setwd(<a git repo>); save_api_key()opens/creates<repo>/.Renviron— an untracked file onegit add -f/git add .away from being committed, with the only protection being the message the user may not read.Expected vs. actual
0644-mode plaintext key file; project-level placement preferred with only advisory protection; no keyring path documented.Suggested fix
file.create(path), applySys.chmod(path, "0600")(and optionally warn if an existing.Renvironis group/world-readable)..Renvironto the repo's own.gitignoreas an example for contributors.keyringpackage) in?get_api_keyfor users who want encrypted storage.I checked for prior reports (issues #181 "Figure out better API key management", #295, #339 — the last fixed a
save_api_keycrash, not the storage posture) and found none covering file permissions or the project-file preference.