credentials/ 0000755 0001762 0000144 00000000000 14677761043 012566 5 ustar ligges users credentials/MD5 0000644 0001762 0000144 00000002425 14677761043 013101 0 ustar ligges users 9539a813954e181c3c5d9d6a81a1d62c *DESCRIPTION
27b6128fe44dac890bf5c9dfce3a744c *LICENSE
3f9a1504712d83a9346263ef9a199b3f *NAMESPACE
dd5de86cd7839fdc6603fe4897f84943 *NEWS
c98ed9fc412b8a0b960765cd74a7fb41 *R/credential-api.R
b942663e50d2db1a6a126c020a92529a *R/credential-helpers.R
8886a4fe29155dfcfa6b35a16079a232 *R/github-pat.R
c928b0f6fb79574cec5af42c164e9e98 *R/http-credential.R
8c1525be85a6e6b474299ea82b139be8 *R/onattach.R
addb008761e123649a869d74bd657cff *R/ssh-keys.R
56a6ffdf930a5e4732c01ba7fd06b466 *build/vignette.rds
80410b923c609a1212e5246e9ebd02a0 *inst/WORDLIST
226d0cc912b9c84fc5942b67eda3860c *inst/ask_token.sh
62bbbc91a860d73b39719b46285e2dea *inst/doc/intro.Rmd
9b8fc18e15d5c0f5b809430e901991c3 *inst/doc/intro.html
a962d9619a3b1302bc2fdde3597b8dea *man/credential_api.Rd
0fdfc6bd70ccd814015d18b80e7b0f69 *man/credential_helper.Rd
d80c5e9d7ec4f849f296c7d3f15967bc *man/figures/logo.png
796553a137098ec948b9cad44e337fb1 *man/http_credentials.Rd
6f3753307764fbe501ae52773173fd2b *man/set_github_pat.Rd
a53160aea5b019b65b35c815dbb311b8 *man/ssh_credentials.Rd
62bbbc91a860d73b39719b46285e2dea *vignettes/intro.Rmd
7617eb7e3c72c58e099e68f2a8727d5b *vignettes/intro.Rmd.in
c6d62931c246f7f97fc1209544b1feeb *vignettes/keygen.png
41748938c3df6c35beedddc655728837 *vignettes/wincred.png
credentials/R/ 0000755 0001762 0000144 00000000000 14677335662 012773 5 ustar ligges users credentials/R/http-credential.R 0000644 0001762 0000144 00000004270 14677335662 016210 0 ustar ligges users #' Load and store git HTTPS credentials
#'
#' This requires you have the `git` command line program installed.The
#' [git_credential_ask] function looks up a suitable username/password
#' from the [`git-credential` store](https://git-scm.com/docs/gitcredentials).
#' If none are available it will prompt the user for credentials which
#' may be saved the store. On subsequent calls for the same URL, the
#' function will then return the stored credentials without prompting
#' the user.
#'
#' The appearance and security policy of the credential store depends
#' on your version of git, your operating system, your R frontend and
#' which [credential_helper] is used. On Windows and MacOS the credentials
#' are stored in the system password manager by default.
#'
#' It should be assumed that reading credentials always involves user
#' interaction. The user may be asked to unlock the system keychain or
#' enter new credentials. In reality, user interaction is usually only
#' required on the first authentication attempt, but the security policy
#' of most credential helpers prevent you from programmatically testing
#' if the credentials are already unlocked.
#'
#' @export
#' @family credentials
#' @rdname http_credentials
#' @name http_credentials
#' @aliases credentials
#' @param url target url, possibly including username or path
#' @param save in case the user is prompted for credentials, attempt to
#' remember them.
#' @param verbose print errors from `git credential` to stdout
git_credential_ask <- function(url = "https://github.com", save = TRUE, verbose = TRUE){
cred <- parse_url(url)
out <- credential_fill(cred = cred, verbose = verbose)
if(isTRUE(save) && length(out) && length(out$password) && !is.na(out$password))
credential_approve(out, verbose = verbose)
out
}
#' @export
#' @rdname http_credentials
git_credential_update <- function(url = "https://github.com", verbose = TRUE){
cred <- parse_url(url)
credential_reject(cred)
out <- credential_fill(cred = cred, verbose = verbose)
credential_approve(out)
}
#' @export
#' @rdname http_credentials
git_credential_forget <- function(url = "https://github.com", verbose = TRUE){
cred <- parse_url(url)
credential_reject(cred)
}
credentials/R/onattach.R 0000644 0001762 0000144 00000005105 14677335662 014720 0 ustar ligges users # We load 'askpass' for its side effects, see below!
#' @importFrom askpass ssh_askpass
.onLoad <- function(libname, pkgname){
# Loading askpass automatically sets SSH_ASKPASS and GIT_ASKPASS variables
askpass::ssh_askpass()
# Note: isatty(stdin()) = TRUE in Windows RGui
if(is_windows() || !interactive() || !isatty(stdin())){
if(is.na(Sys.getenv('GIT_TERMINAL_PROMPT', NA))){
Sys.setenv(GIT_TERMINAL_PROMPT=0)
}
}
# Start ssh-agent if available but not running
if(is_windows() && is.na(Sys.getenv('SSH_AGENT_PID', NA)) && cmd_exists('start-ssh-agent')){
# Agent not work in Windows: https://github.com/libgit2/libgit2/issues/4958
#ssh_agent_start()
}
# If no credential helper has been set, use the 'cache' helper
if(!is_check())
set_default_cred_helper()
}
.onAttach <- function(libname, pkgname){
tryCatch({
gitver <- git_with_sys("--version", NULL, FALSE)
packageStartupMessage(sprintf("Found %s", gitver))
helpers <- sub("^credential-", "", credential_helper_list())
packageStartupMessage(sprintf("Supported HTTPS credential helpers: %s",
paste(helpers, collapse = ", ")))
}, error = function(e){
if(is_windows()){
packageStartupMessage("Git for Windows is not installed.\nDownload from: https://git-scm.com/download/win")
} else {
packageStartupMessage("Unable to find git :-(")
}
})
tryCatch({
sshver <- ssh_version()
packageStartupMessage(sprintf("Found %s", sshver))
tryCatch({
key <- find_ssh_key()
if(length(key)){
packageStartupMessage(sprintf("Default SSH key: %s", key))
} else {
packageStartupMessage("No key found. Use ssh_keygen() to generate one!")
}
}, error = function(e){
packageStartupMessage("Failed to lookup key file")
})
}, error = function(e){
packageStartupMessage(e$message)
})
#agent_output <- ssh_agent_start()
#if(length(agent_output) && nchar(agent_output)){
# packageStartupMessage(trimws(agent_output))
#}
}
set_default_cred_helper <- function(){
if(has_git_cmd()){
invisible(tryCatch({
credential_helper_get()
}, error = function(...){
tryCatch({
helpers <- credential_helper_list()
if(identical(.Platform$OS.type, 'windows') && 'credential-manager' %in% helpers)
helpers <- 'credential-manager'
credential_helper_set(helpers[1], global = TRUE)
}, error = function(e){
packageStartupMessage(e$message)
})
}))
}
}
is_check <- function(){
grepl('credentials.Rcheck', getwd(), fixed = TRUE)
}
credentials/R/credential-api.R 0000644 0001762 0000144 00000013774 14677335662 016013 0 ustar ligges users #' Retrieve and store git HTTPS credentials
#'
#' Low-level wrappers for the [git-credential](https://git-scm.com/docs/git-credential)
#' command line tool. Try the user-friendly [git_credential_ask]
#' and [git_credential_update] functions first.
#'
#' The [credential_fill] function looks up credentials for a given host, and
#' if none exists it will attempt to prompt the user for new credentials. Upon
#' success it returns a list with the same `protocol` and `host` fields as the
#' `cred` input, and additional `username` and `password` fields.
#'
#' After you have tried to authenticate the provided credentials, you can report
#' back if the credentials were valid or not. Call [credential_approve] and
#' [credential_reject] with the `cred` that was returned by [credential_fill]
#' in order to validate or invalidate a credential from the store.
#'
#' Because git credential interacts with the system password manager, the appearance
#' of the prompts vary by OS and R frontend. Note that [credential_fill] should
#' only be used interactively, because it may require the user to enter credentials
#' or unlock the system keychain. On the other hand [credential_approve] and
#' [credential_reject] are non-interactive and could be used to save or delete
#' credentials in a scripted program. However note that some credential helpers
#' (e.g. on Windows) have additional security restrictions that limit use of
#' [credential_approve] and [credential_reject] to credentials that were actually
#' entered by the user via [credential_fill]. Here it is not possible at all to
#' update the credential store without user interaction.
#'
#' @export
#' @rdname credential_api
#' @name credential_api
#' @param cred named list with at least fields `protocol` and `host` and
#' optionally also `path`, `username` ,`password`.
#' @param verbose emit some useful output about what is happening
#' @examples \donttest{
#' # Insert example cred
#' example <- list(protocol = "https", host = "example.org",
#' username = "test", password = "secret")
#' credential_approve(example)
#'
#' # Retrieve it from the store
#' cred <- credential_fill(list(protocol = "https", host = "example.org", path = "/foo"))
#' print(cred)
#'
#' # Delete it
#' credential_reject(cred)
#' }
credential_fill <- function(cred, verbose = TRUE){
out <- credential_exec("fill", cred = cred, verbose = verbose)
data <- strsplit(out, "=", fixed = TRUE)
key <- vapply(data, `[`, character(1), 1)
val <- vapply(data, `[`, character(1), 2)
structure(as.list(structure(val, names = key)), class = 'git_credential')
}
#' @export
#' @rdname credential_api
#' @name credential_api
credential_approve <- function(cred, verbose = TRUE){
credential_exec("approve", cred = cred, verbose = verbose)
invisible()
}
#' @export
#' @rdname credential_api
#' @name credential_api
credential_reject <- function(cred, verbose = TRUE){
credential_exec("reject", cred = cred, verbose = verbose)
invisible()
}
credential_exec <- function(command, cred, verbose){
input <- cred_to_input(cred)
on.exit(unlink(input))
if(is_windows() || is_macos() || !isatty(stdin())){
text <- git_with_sys(c("credential", command), input = input, verbose = verbose)
strsplit(text, "\n", fixed = TRUE)[[1]]
} else {
# base::system can freeze RStudio Desktop or Windows
git_with_base(c("credential", command), input = input, verbose = verbose)
}
}
git_with_base <- function(command, input = "", verbose = TRUE){
git <- find_git_cmd()
res <- system2(git, command, stdin = input,
stdout = TRUE, stderr = ifelse(isTRUE(verbose), "", TRUE))
status <- attr(res, "status")
if(length(status) && !identical(status, 0L)){
stop(paste(res, collapse = "\n"))
}
res
}
git_with_sys <- function(command, input = NULL, verbose = TRUE){
git <- find_git_cmd()
outcon <- rawConnection(raw(0), "r+")
on.exit(close(outcon), add = TRUE)
timeout <- ifelse(interactive(), 120, 10)
status <- sys::exec_wait(git, command, std_out = outcon, std_err = verbose,
std_in = input, timeout = timeout)
if(!identical(status, 0L)){
stop(sprintf("Failed to call 'git %s'", paste(command, collapse = " ")), call. = FALSE)
}
trimws(rawToChar(rawConnectionValue(outcon)))
}
find_git_cmd <- function(git = getOption("git", "git"), error = TRUE){
if(cmd_exists(git)){
return(git)
}
if(is_windows()){
locations <- c("C:\\PROGRA~1\\Git\\cmd\\git.exe",
"C:\\Program Files\\Git\\cmd\\git.exe")
for(i in locations){
if(cmd_exists(i)){
return(i)
}
}
}
if(error){
stop(sprintf("Could not find the '%s' command line util", git), call. = FALSE)
}
}
has_git_cmd <- function(){
!is.null(find_git_cmd(error = FALSE))
}
parse_url <- function(url, allow_ssh = TRUE){
out <- strsplit(url, "://", fixed = TRUE)[[1]]
if(length(out) < 2){
if(!isTRUE(allow_ssh)){
stop(sprintf("URL must start with e.g. https:// (found %s)", url))
} else {
protocol = 'ssh'
rest = url
}
} else {
protocol <- out[1]
rest <- out[2]
}
password <- NULL
username <- if(grepl("^[^/]+@", rest)){
auth <- strsplit(rest, "@", fixed = TRUE)[[1]]
rest <- paste(auth[-1], collapse = "@")
password <- if(grepl(":", auth[1], fixed = TRUE)){
auth <- strsplit(auth[1], ":", fixed = TRUE)[[1]]
paste(auth[-1], collapse = ":")
}
auth[1]
}
rest <- strsplit(rest, "/", fixed = TRUE)[[1]]
host <- rest[1]
path <- if(length(rest) > 1){
paste(rest[-1], collapse = "/")
}
c(
username = username,
password = password,
protocol = protocol,
host = host,
path = path
)
}
cred_to_input <- function(data, input = tempfile()){
str <- paste(names(data), as.character(data), collapse = "\n", sep = "=")
writeBin(charToRaw(sprintf("%s\n", str)), con = input)
return(input)
}
cmd_exists <- function(cmd){
nchar(Sys.which(cmd)) > 0
}
is_windows <- function(){
identical(.Platform$OS.type, "windows")
}
is_macos <- function(){
identical(tolower(Sys.info()[['sysname']]), "darwin")
}
credentials/R/ssh-keys.R 0000644 0001762 0000144 00000021001 14677335662 014656 0 ustar ligges users #' Managing Your SSH Key
#'
#' Utility functions to find or generate your SSH key for use with git remotes
#' or other ssh servers.
#'
#' Use [ssh_key_info()] to find the appropriate key file on your system to connect with a
#' given target host. In most cases this will simply be `ssh_home('id_rsa')` unless
#' you have configured ssh to use specific keys for specific hosts.
#'
#' To use your key to authenticate with GitHub, copy the pubkey from `ssh_key_info()` to
#' your profile: \url{https://github.com/settings/ssh/new}.
#'
#' If this is the first time you use ssh, [ssh_keygen] can help generate a key and
#' save it in the default location. This will also automatically opens the above Github
#' page in your browser where you can add the key to your profile.
#'
#' `ssh_read_key` reads a private key and caches the result (in memory) for the
#' duration of the R session. This prevents having to enter the key passphrase many
#' times. Only use this if `ssh-agent` is not available (i.e. Windows)
#'
#' @export
#' @family credentials
#' @rdname ssh_credentials
#' @name ssh_credentials
#' @param host target host (only matters if you have configured specific keys per host)
#' @param auto_keygen if `TRUE` automatically generates a key if none exists yet.
#' Default `NA` is to prompt the user what to.
ssh_key_info <- function(host = NULL, auto_keygen = NA){
keyfile <- find_ssh_key(host = host)
if(is.null(keyfile)){
if(isTRUE(auto_keygen) || (is.na(auto_keygen) && ask_user("No SSH key found. Generate one now?"))){
keyfile <- ssh_home('id_ecdsa')
ssh_keygen(keyfile)
} else {
stop(sprintf("Failed to find ssh key file for %s", host))
}
}
pubfile <- paste0(keyfile, ".pub")
if(!file.exists(pubfile)){
key <- ssh_read_key(keyfile)
try(openssl::write_ssh(key$pubkey, pubfile), silent = TRUE)
}
list(
key = keyfile,
pubkey = openssl::write_ssh(openssl::read_pubkey(pubfile))
)
}
#' @export
#' @rdname ssh_credentials
#' @param file destination path of the private key. For the public key, `.pub`
#' is appended to the filename.
#' @importFrom openssl write_ssh write_pem read_key write_pkcs1 read_pubkey
ssh_keygen <- function(file = ssh_home('id_ecdsa')){
private_key <- normalizePath(file, mustWork = FALSE)
pubkey_path <- paste0(private_key, ".pub")
if(file.exists(private_key)){
cat(sprintf("Found existing RSA keyspair at: %s\n", private_key), file = stderr())
pubkey <- if(file.exists(pubkey_path)){
read_pubkey(pubkey_path)
} else {
ssh_read_key(private_key)$pubkey
}
} else {
cat(sprintf("Generating new RSA keyspair at: %s\n", private_key), file = stderr())
key <- openssl::ec_keygen("P-521")
pubkey <- key$pubkey
dir.create(dirname(private_key), showWarnings = FALSE)
write_pkcs1(key, private_key)
write_ssh(pubkey, pubkey_path)
Sys.chmod(private_key, "0600")
}
# See https://help.github.com/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent/
conf_file <- file.path(dirname(private_key), 'config')
if(is_macos() && !file.exists(conf_file)){
writeLines(c('Host *', ' AddKeysToAgent yes', ' UseKeychain yes',
paste(' IdentityFile ', private_key)), con = conf_file)
}
list(
key = private_key,
pubkey = write_ssh(pubkey)
)
}
#' @rdname ssh_credentials
#' @export
ssh_setup_github <- function(){
info <- ssh_key_info()
cat("Your public key:\n\n", info$pubkey, "\n\n", file = stderr())
cat("Please copy the line above to GitHub: https://github.com/settings/ssh/new\n", file = stderr())
if(interactive() && ask_user('Would you like to open a browser now?')){
utils::browseURL('https://github.com/settings/ssh/new')
}
}
#' @export
#' @rdname ssh_credentials
ssh_home <- function(file = NULL){
if(length(file)){
normalizePath(file.path(normalize_home("~/.ssh"), file), mustWork = FALSE)
} else {
normalize_home("~/.ssh")
}
}
#' @export
#' @rdname ssh_credentials
ssh_agent_add <- function(file = NULL){
if(is.na(Sys.getenv('SSH_AUTH_SOCK', NA))){
stop("ssh-agent is not running. If this is a server ssh in with: ssh -o 'ForwardAgent=yes'")
}
sys::exec_wait('ssh-add', as.character(file)) == 0
}
# Only used on Windows for now
ssh_agent_start <- function(){
if(is_windows()){
out <- sys::exec_internal('cmd', c('/C', 'start-ssh-agent 1>&2 && set'), error = FALSE)
if(out$status != 0){
warning("Failed to start ssh-agent", call. = FALSE)
} else{
con <- rawConnection(out$stdout)
on.exit(close(con))
vars <- readLines(con)
vars <- vars[grepl('^SSH_(AGENT|AUTH)', vars)]
if(length(vars)){
tmp <- tempfile()
writeLines(vars, tmp)
readRenviron(tmp)
}
}
return(rawToChar(out$stderr))
}
}
find_ssh_key <- function(host = NULL){
if(!length(host))
host <- "*"
key_paths <- tryCatch(ssh_identityfiles(host = host), error = function(e){
message(e$message)
file.path("~/.ssh", c("id_ecdsa", "id_ed25519", "id_rsa", "id_dsa", "id_xmss"))
})
key_paths <- normalize_home(key_paths)
for(i in key_paths){
if(file.exists(i))
return(i)
}
return(NULL)
}
ssh_identityfiles <- function(host){
# Note there can be multiple 'identityfile' entries
conf <- ssh_config(host = host)
candidates <- unique(unlist(conf[names(conf) == 'identityfile']))
candidates <- Filter(function(x){!grepl('id_dsa', x)}, candidates)
sort(candidates) #prefer ecdsa, ed25519, rsa
}
# Old SSH versions (Trusty, CentOS) do not support ssh -G
ssh_config <- function(host){
ssh <- find_ssh_cmd()
out <- sys::exec_internal(ssh, c("-G", host), error = FALSE)
if(!identical(out$status, 0L))
stop("Could not read ssh config. Using default settings.", call. = FALSE)
txt <- strsplit(rawToChar(out$stdout), "\r?\n")[[1]]
lines <- strsplit(txt, " ", fixed = TRUE)
names <- vapply(lines, `[`, character(1), 1)
values <- lapply(lines, `[`, -1)
structure(values, names = names)
}
ssh_version <- function(){
ssh <- find_ssh_cmd()
out <- sys::exec_internal(ssh, "-V")
# ssh may print to stderr instead of stdout
trimws(rawToChar(c(out$stdout, out$stderr)))
}
find_ssh_cmd <- function(ssh = getOption("ssh", "ssh")){
if(cmd_exists(ssh))
return(ssh)
if(is_windows()){
# Try asking 'git.exe' where it looks for 'ssh.exe
try({
gitssh <- git_with_sys(c("-c", "alias.sh=!sh", "sh", "-c", "cygpath -m $(which ssh)"))
if(nchar(gitssh) && cmd_exists(gitssh))
return(Sys.which(gitssh))
}, silent = TRUE)
# Fallback: try to find ssh.exe ourselves in the usual places
try({
bin <- dirname(find_git_cmd())
usrbin <- file.path(dirname(bin), "usr", "bin")
path <- Sys.getenv('PATH')
on.exit(Sys.setenv(PATH = path))
Sys.setenv(PATH = paste(path, bin, usrbin, sep = .Platform$path.sep))
if(cmd_exists(ssh))
return(Sys.which(ssh))
}, silent = TRUE)
}
stop(sprintf("No '%s' command found. Using default ssh settings.", ssh), call. = FALSE)
}
normalize_home <- function(path = NULL){
path <- as.character(path)
if(is_windows()){
homedir <- Sys.getenv('USERPROFILE')
is_home <- grepl("^~", path)
path[is_home] <- paste0(homedir, substring(path[is_home], 2))
}
normalizePath(path, mustWork = FALSE)
}
ask_user <- function(str){
if(!interactive())
return(FALSE)
return(utils::menu(c("Yes", "No"), title = str) == 1)
}
#' @export
#' @rdname ssh_credentials
ssh_update_passphrase <- function(file = ssh_home("id_rsa")){
key <- openssl::read_key(file, password = function(x){
askpass::askpass("Please enter your _current_ passphrase")
})
new1 <- askpass::askpass("Enter the _new_ passphrase")
new2 <- askpass::askpass("To confirm, the your _new_ passphrase again")
if(identical(new1, new2)){
openssl::write_pkcs1(key, path = file, password = new1)
} else {
stop("Entered passhprases are not identical")
}
message("Passphrase has been updated!")
# Wipe the key cache just in case
environment(ssh_read_key)$store = new.env(parent = emptyenv())
}
#' @export
#' @rdname ssh_credentials
#' @param password a passphrase or callback function
#' @importFrom askpass askpass
ssh_read_key <- local({
store = new.env(parent = emptyenv())
function (file = ssh_home("id_rsa"), password = askpass){
file <- normalizePath(file, mustWork = TRUE)
hash <- openssl::md5(file)
if(!length(store[[hash]])){
store[[hash]] <- tryCatch(openssl::read_key(file, password = password), error = function(e){
stop(sprintf("Unable to load key: %s", file), call. = FALSE)
})
}
return(store[[hash]])
}
})
credentials/R/credential-helpers.R 0000644 0001762 0000144 00000002315 14677335662 016671 0 ustar ligges users #' Credential Helpers
#'
#' Git supports several back-end stores for HTTPS credentials called
#' helpers. Default helpers include `cache` and `store`, see the
#' [git-credentials](https://git-scm.com/docs/gitcredentials) manual
#' page for details.
#'
#' @export
#' @rdname credential_helper
#' @name credential_helper
credential_helper_list <- function(){
text <- git_with_sys(c("help", "-a"))
m <- gregexpr("credential-[^ \t]+", text)
trimws(regmatches(text, m)[[1]])
}
#' @export
#' @rdname credential_helper
#' @name credential_helper
#' @param global if FALSE the setting is done per git repository, if
#' TRUE it is in your global user git configuration.
credential_helper_get <- function(global = FALSE){
git <- find_git_cmd()
args <- c("config", if(global) "--global", "credential.helper")
git_with_sys(args)
}
#' @export
#' @rdname credential_helper
#' @name credential_helper
#' @param helper string with one of the supported helpers from [credential_helper_list]
credential_helper_set <- function(helper, global = FALSE){
helper <- sub("^credential-", "", helper)
args <- c("config", if(global) "--global", "credential.helper", helper)
git_with_sys(args)
credential_helper_get(global = global)
}
credentials/R/github-pat.R 0000644 0001762 0000144 00000006354 14677335662 015172 0 ustar ligges users #' Set your Github Personal Access Token
#'
#' Populates the `GITHUB_PAT` environment variable using the [git_credential][http_credentials]
#' manager, which `git` itself uses for storing passwords. The credential manager
#' returns stored credentials if available, and securely prompt the user for
#' credentials when needed.
#'
#' Packages that require a `GITHUB_PAT` can call this function to automatically
#' set the `GITHUB_PAT` when needed. Users may call this function in their
#' [.Rprofile][Startup] script to automatically set `GITHUB_PAT` for each R
#' session without hardcoding any tokens on disk in plain-text.
#'
#' @export
#' @param force_new forget existing pat, always ask for new one.
#' @param validate checks with the github API that this token works. Defaults to
#' `TRUE` only in an interactive R session (not when running e.g. CMD check).
#' @param verbose prints a message showing the credential helper and PAT owner.
#' @return Returns `TRUE` if a valid GITHUB_PAT was set, and FALSE if not.
set_github_pat <- function(force_new = FALSE, validate = interactive(), verbose = validate){
pat_user <- Sys.getenv("GITHUB_PAT_USER", 'PersonalAccessToken')
pat_url <- sprintf('https://%s@github.com', pat_user)
if(isTRUE(force_new))
git_credential_forget(pat_url)
if(isTRUE(verbose))
message("If prompted for GitHub credentials, enter your PAT in the password field")
askpass <- Sys.getenv('GIT_ASKPASS')
if(nchar(askpass)){
# Hack to override prompt sentence to say "Token" instead of "Password"
Sys.setenv(GIT_ASKTOKEN = askpass)
Sys.setenv(GIT_ASKPASS = system.file('ask_token.sh', package = 'credentials', mustWork = TRUE))
Sys.setenv(GIT_ASKTOKEN_NAME = 'Personal Access Token (PAT)')
on.exit(Sys.setenv(GIT_ASKPASS = askpass), add = TRUE)
on.exit(Sys.unsetenv('GIT_ASKTOKEN_NAME'), add = TRUE)
on.exit(Sys.unsetenv('GIT_ASKTOKEN'), add = TRUE)
}
for(i in 1:3){
# The username doesn't have to be real, Github seems to ignore username for PATs
cred <- git_credential_ask(pat_url, verbose = verbose)
if(length(cred$password)){
if(nchar(cred$password) < 40){
message("Please enter a token in the password field, not your master password! Let's try again :-)")
message("To generate a new token, visit: https://github.com/settings/tokens")
credential_reject(cred)
next
}
if(isTRUE(validate)) {
hx <- curl::handle_setheaders(curl::new_handle(), Authorization = paste("token", cred$password))
req <- curl::curl_fetch_memory("https://api.github.com/user", handle = hx)
if(req$status_code >= 400){
message("Authentication failed. Token invalid.")
credential_reject(cred)
next
}
if(verbose == TRUE){
data <- jsonlite::fromJSON(rawToChar(req$content))
helper <- tryCatch(credential_helper_get()[1], error = function(e){"??"})
message(sprintf("Using GITHUB_PAT from %s (credential helper: %s)", data$name, helper))
}
}
return(Sys.setenv(GITHUB_PAT = cred$password))
}
}
if(verbose == TRUE){
message("Failed to find a valid GITHUB_PAT after 3 attempts")
}
return(FALSE)
}
message <- function(...){
base::message(...)
utils::flush.console()
}
credentials/NEWS 0000644 0001762 0000144 00000002350 14677504723 013265 0 ustar ligges users 2.0.1
- Precompute vignette due to credential issues on some CRAN machines.
2.0.0
- ssh_key_info() now returns ECDSA instead of RSA key by default if both exist.
If you get authentication problems with 'gert' you probably need to upload your
ecdsa key to your host (e.g. using ssh_setup_github()) or otherwise delete the
ECDSA key from ~/.ssh/ecdsa to go back and use your old RSA key.
- ssh_keygen() now defaults to generating ECDSA (P-521) keys
1.3.2
- Disable example in vignette that would prompt for user input on Windows
1.3.1
- Set permission to user-read only for generated private keys
1.3.0
- PATs stored with set_github_pat() are now stored under username 'PersonalAccessToken'
to match the new Git Credential Manager Core behavior. You may have to re-enter your PAT.
- Small UX improvements.
1.2.1
- Quote path to $GIT_ASKTOKEN, for example when credentials is installed in "Program Files"
- Fix for set_github_pat() token prompt in R.app on MacOS
1.2.0
- Bug fixes and tweaks for set_github_pat(): gains parameters to disable validation
1.1
- Fix for vignette when no 'git' is installed
- Fix for vignette when credential helper requires user interaction (CRAN MAC)
1.0
- Initial CRAN release
credentials/vignettes/ 0000755 0001762 0000144 00000000000 14677523120 014566 5 ustar ligges users credentials/vignettes/intro.Rmd.in 0000644 0001762 0000144 00000016751 14677335662 017020 0 ustar ligges users ---
title: "Managing SSH and Git Credentials in R"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Managing SSH and Git Credentials in R}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
The `credentials` package contains tools for configuring and retrieving SSH and HTTPS credentials for use with `git` or other services. It helps users to setup their git installation, and also provides a back-end for packages to authenticate with existing user credentials.
## Two types of remotes
```{r, echo=FALSE}
delete_git_config_on_exit <- !file.exists('~/.gitconfig')
credentials:::set_default_cred_helper()
library <- function(package){
withCallingHandlers(base::library(credentials),
packageStartupMessage = function(e) {
cat(e$message)
invokeRestart("muffleMessage")
})
}
```
```{r}
library(credentials)
```
Git supports two types of remotes: SSH and HTTPS. These two use completely distinct authentication systems.
| | SSH REMOTES | HTTPS REMOTES |
|---------|------------------|------------------------|
| __url__ | `git@server.com` | `https://server.com` |
| __authentication__ | Personal SSH key | Password or PAT |
| __stored in__ | file `id_rsa` or `ssh-agent` | `git credential` store |
For HTTPS remotes, git authenticates with a username + password. With GitHub, instead of a password, you can also use a [Personal Access Token](https://github.com/settings/tokens) (PAT). PATs are preferred because they provide more granular control of permissions (via the PAT's scopes), you can have many of them for different purposes, you can give them informative names, and they can be selectively revoked. Note that, if you use 2FA with GitHub, you must authenticate with a PAT if you use the HTTPS protocol.
For SSH remotes, git shells out to `ssh` on your system, and lets `ssh` take care of authentication. This means you have to setup an ssh key (usually `~/.ssh/id_rsa`) which you then [add to your git profile](https://github.com/settings/ssh/new).
### Special note for Windows
Windows does not include a native `git` installation by default. We recommended to use the latest version of [Git for Windows](https://git-scm.com/download/win). This bundle also includes `ssh` and [git credential manager for windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows) which is all you need.
Important: ssh keys are stored in your home directory for example: `C:\Users\Jeroen\.ssh\id_rsa`, and __not in the Documents folder__ (which is what R treats as the home sometimes). The `ssh_home()` function shows the correct `.ssh` directory on all platforms.
## Part 1: Storing HTTPS credentials
HTTPS remotes do not always require authentication. You can clone from a public repository without providing any credentials. But for pushing, or private repositories, `git` will prompt for a username/password.
```
git clone https://github.com/jeroen/jsonlite
```
To save you from entering your password over and over, git includes a [credential helper](https://git-scm.com/docs/gitcredentials). It has two modes:
- `cache`: Cache credentials in memory for a short period of time.
- `store`: Store credentials permanently in your operating system password manager.
To see which helper is configured for a given repo, run:
```{r}
credential_helper_get()
```
Most `git` installations default to `store` if supported because it is more convenient and secure. However the look and policy of the git credential store for entering and retrieving passwords can vary by system, because it uses the OS native password manager.
### Accessing the HTTPS Credential Store from R
The `credentials` R package provides a wrapper around the `git credential` command line API for reading and saving credentials. The `git_credential_ask()` function looks up suitable credentials for a given URL from the store. If no credentials are available, it will attempt to prompt the user for credentials and return those instead.
```{r, echo=FALSE}
# This hack may not work on MacOS server where cred helper is osxkeychain
# which always requires user interaction. Hence error=TRUE in the next block.
example <- list(protocol = "https", host = "example.com",
username = "jeroen", password = "supersecret")
credential_approve(example)
```
```{r error=TRUE}
library(credentials)
git_credential_ask('https://example.com')
```
The function `git_credential_update()` looks similar but it behaves slightly different: it first removes existing credentials from the store (if any), then prompts the user for a new username/password, and saves these to the store.
```r
# This should always prompt for new credentials
git_credential_update('https://example.com')
```
In a terminal window this will result in an interactive password prompt. In Windows the user might see something like this (depending on the version of Windows and git configuration):
```{r, echo=FALSE}
credential_reject(list(protocol = "https", host = "example.com"))
```
### Setting your GITHUB_PAT
Automatically populate your `GITHUB_PAT` environment variable from the native git credential store. The credential manager will safely prompt the user for credentials when needed.
```r
credentials::set_github_pat()
## Using GITHUB_PAT from Jeroen Ooms (credential helper: osxkeychain)
```
Use this function in your `.Rprofile` if you want to automatically set `GITHUB_PAT` for each R session, without hardcoding your secrets in plain text, such as in your `.Renviron` file.
### Non-interactive use
Retrieving credentials is by definition interactive, because the user may be required to enter a password or unlock the system keychain. However, saving or deleting credentials can sometimes be done non-interactively, but this depends on which credential helper is used.
The manual page for `credential_approve` and `credential_reject` has more details about how to call the basic git credential api.
## Part 2: Managing SSH Keys
```{r, echo = FALSE}
ssh_key_info <- function(){
try({
out <- credentials:::ssh_key_info(auto_keygen = FALSE)
out$pubkey = paste(substring(out$pubkey, 1, 80), "...")
out
})
}
```
For SSH remotes, git does not handle authentication itself. Git simply shells out to `ssh` on your system and uses your standard user ssh configuration. Hence authenticating with SSH git remotes comes down to setting up your ssh keys and copying these to your profile.
The `credentials` package provides a few utility functions to make this easier. The `ssh_key_info()` function calls out to look up which key `ssh` uses to connect to a given server. This is usually `~/.ssh/id_rsa` unless you have a fancy custom ssh configuration.
```{r}
ssh_key_info()
```
The output shows both the path to your (private) key as well as the ssh pubkey string. The latter is what you have to enter in your [GitHub profile](https://github.com/settings/ssh/new) to associate this key with your user account. You will then be automatically authenticated when using GitHub SSH remotes.
### Generating a key
To use SSH you need a personal key, which is usually stored in `~/.ssh/id_rsa`. If you do not have a key yet, the `ssh_key_info()` function will automatically ask if you want to generate one.
You can also generate a key manually elsewhere using the `ssh_keygen()` function.
```{r echo=FALSE}
if(isTRUE(delete_git_config_on_exit))
unlink("~/.gitconfig")
```
credentials/vignettes/keygen.png 0000755 0001762 0000144 00000075451 14677335662 016611 0 ustar ligges users PNG
IHDR x " jk sRGB gAMA a pHYs % %IR$ zIDATx^kyX9dR%[FZ 7&b䙘ر=;1c
MH%{IEAw A\w4 I$wɔ,On}YUdeeVe֩}D8}22*_Ӌ~GG!B!Ʌk%o?ަwbJuϩϿB!BhugԦ͛3ޢ~Vuj߽B!B- =|)r
>mN<B!BhILx:mHOv B!ZxZz~
݁!B!ZzշO88B!B;lD!B!T2x{!B!TIB!BP<B!0x!B!aB!B#!B!Ԃf<]}V/8I?}-} !Bt5ծ=$qKed%D%w䁧K*Wu_TQ-Y\"B!P$O\N>s삓-ӗђݗbrZ.#KI+kC!Bh!oX;{Ϲ'[I|=Uo
I=ztm쾄h`g;ɶNuMʍԚ'f.7{f]>B!xcNwu/>\tgL[cIYQ>-K2x;x1KyVoQ7,_nجvܣ?8v=g
+תoXn}}י^="nB!?V=,zbIܗsEq7W߄\eeee%D?C&O_Vm6}Sm16oVP[Tۧgj.5-[w[ߡiԕ<B!xEu(1M]Щ';?VҁS~~tsÉzg,ˮt}KKȎ=lJbe$n/!Fljvu)uНqۣۛ>[WSժuk6&ڬ֮ߚU3$Fp[,ԍB!B657E,#=HblxW!rմ/OjrO~}LnlTNL?̝٣ڰubbnrCK7}?O]p؇Bё62Xr9Sn}iMLޮcg#M
^ޗ
d6oߩ9wQ{n^uM+~yx橽5ԵvQ˒V+7TQPܰBMmخ]wYgwĸ$^[5~ݣ.TYoMыT_/?4h8.K}֟,Q!BÐ6<-15XrguiH}WԖY~faWSǎPwnjNuۆ2+T ~Lk}KGrTΗvFu57ޮ6nѿgՓ^ۋ|Q24bױO0MjwObԟHsfҜMgZ.Kj3z]M˕
B͝i/ٓ]~κԶx\MT/2j>%DUk7=jړWnf<_WOQdvߺpE/P_r@}lW6dGz-[ղkWW\ucDq*lmڐkx7GcrcZPK䉭vC!iyw{7ޱUݲvS`D{Ŕ\&')/\[uW$sqv{ehmwLd ]f##_:>t>KߺxFWU/Q_W}hb~jun)U7Av=ѫP4䫉3.:-B-}u'^VGx)h*9Uo$nH;|5yMꪥ)ȷ/m}3Q!niZ3ˎ\hsgƥkw8!/QeUf4sզj<3V!
S2]keC$;=*[J44xz}O=w={:_|+߸Lݰjv6wf>LnZZ
wwwpy.#O}]I^2vH;CWl:/VuuOݵ Y&ƶoOZ\MMYgMߖ-jK\QROfԣXNv_l~mZ_UK?G"]qv)+WΫOX1X1۬xWBܑmJs͏t[xB!40EJs]Hٗ^x'y_gbW̬ۗ&*ޟAxk%?Q3GOÉ7} Q;lnYMڳ_ڬ6nSn^:~]3}D}01vpVnuw/ަ~-nS%iu.ޣ]5Yz~B[=/T?P7௮'mz CHQ,~sNs)=O9alۡ'A24˖kR#W"R{%V=^:C٥]gUұj?^Y\2J B!6LK/Xrv]Hٗ{'y_gbWۗܬY&*]:g}6XR^~wo=h7tM'&cZ~ڸyv5;7'v.ڣ0m_جqu~uOQ?bYfbSKfު>6K,j~RHv=\J)8.5c?P{ݿ\eۤΞd+*w_@}q