├── .Rbuildignore ├── .gitignore ├── .travis.yml ├── DESCRIPTION ├── LICENSE ├── NAMESPACE ├── R ├── agent.R ├── onload.R ├── password.R └── setup.R ├── agent.Rproj ├── appveyor.yml ├── man ├── agent.Rd └── password.Rd └── readme.md /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^readme.md$ 4 | \.travis 5 | appveyor.yml 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .Rproj.user 3 | .Rhistory 4 | .RData 5 | src/*.o 6 | src/*.so 7 | src/*.dll 8 | src/*.a 9 | src/*.def 10 | src/Makevars 11 | tools/option_table.txt 12 | inst/doc 13 | R/sysdata.rda 14 | windows 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: r 2 | latex: false 3 | pandoc: false 4 | fortran: false 5 | 6 | matrix: 7 | include: 8 | - os: linux 9 | env: R_CODECOV=true 10 | - os: osx 11 | brew_packages: openssl 12 | 13 | addons: 14 | apt: 15 | packages: 16 | - libssl-dev 17 | - valgrind 18 | 19 | r_github_packages: 20 | - jeroenooms/jsonlite 21 | - jimhester/covr 22 | 23 | warnings_are_errors: true 24 | #r_check_revdep: true 25 | 26 | notifications: 27 | email: 28 | on_success: change 29 | on_failure: change 30 | 31 | after_success: 32 | - if [[ "${R_CODECOV}" ]]; then R -e 'covr::codecov()'; fi 33 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: agent 2 | Type: Package 3 | Title: Encrypted Key-Value Store for Sensitive Data 4 | Version: 0.1 5 | Author: Jeroen Ooms 6 | Maintainer: Jeroen Ooms 7 | Description: Cross platform solution for securely storing sensitive data. This 8 | can either be used directly by the user or by other packages for storing 9 | e.g. web tokens or other secrets. The degree of security is detemined by 10 | the strength of the password as set by the user. 11 | License: MIT + file LICENSE 12 | Encoding: UTF-8 13 | Imports: 14 | openssl (>= 0.9.5), 15 | rappdirs, 16 | getPass 17 | RoxygenNote: 5.0.1.9000 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2016 2 | COPYRIGHT HOLDER: Jeroen Ooms 3 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(agent_del) 4 | export(agent_get) 5 | export(agent_has) 6 | export(agent_set) 7 | export(reset_password) 8 | export(update_password) 9 | -------------------------------------------------------------------------------- /R/agent.R: -------------------------------------------------------------------------------- 1 | #' Token Agent 2 | #' 3 | #' Safely store and load sensitive data. This API may be both by users as well 4 | #' as packages to store e.g. secrets or auth tokens. 5 | #' 6 | #' Note that it is up to the user to secure the keyring with a password via 7 | #' \link{update_password}. 8 | #' 9 | #' @export 10 | #' @rdname agent 11 | #' @aliases agent 12 | #' @param name a unique name to store and retrieve this object. Make sure to 13 | #' pick a unique name that does not conflict with other packages using the same 14 | #' store. 15 | #' @param value an R object to be stored securely. This part contains the 16 | #' sensitive data. 17 | agent_set <- function(name, value){ 18 | hash <- digest(name) 19 | datafile <- token_datafile(hash) 20 | keyfile <- token_keyfile(hash) 21 | if(file.exists(datafile)) 22 | stop(sprintf("token '%s' already exists", name)) 23 | pubkey <- get_pubkey() 24 | aes <- openssl::rand_bytes(16) 25 | aes_encrypted <- openssl::rsa_encrypt(aes, pubkey = pubkey) 26 | buf <- serialize(value, NULL) 27 | cipher <- openssl::aes_cbc_encrypt(buf, key = aes, iv = hash) 28 | writeBin(as.raw(cipher), datafile) 29 | writeBin(aes_encrypted, keyfile) 30 | } 31 | 32 | #' @export 33 | #' @rdname agent 34 | agent_has <- function(name){ 35 | hash <- digest(name) 36 | datafile <- token_datafile(hash) 37 | file.exists(datafile) 38 | } 39 | 40 | #' @export 41 | #' @rdname agent 42 | agent_get <- function(name){ 43 | hash <- digest(name) 44 | datafile <- token_datafile(hash) 45 | keyfile <- token_keyfile(hash) 46 | if(!file.exists(datafile)) 47 | stop("token does not exist") 48 | key <- get_key() 49 | aes_encrypted <- readBin(keyfile, raw(), file.info(keyfile)$size) 50 | aes <- openssl::rsa_decrypt(aes_encrypted, key = key) 51 | cipher <- readBin(datafile, raw(), file.info(datafile)$size) 52 | out <- openssl::aes_cbc_decrypt(cipher, aes, iv = hash) 53 | unserialize(out) 54 | } 55 | 56 | #' @export 57 | #' @rdname agent 58 | agent_del <- function(name){ 59 | hash <- digest(name) 60 | datafile <- token_datafile(hash) 61 | keyfile <- token_keyfile(hash) 62 | unlink(c(datafile, keyfile)) 63 | } 64 | 65 | digest <- function(x){ 66 | openssl::md5(serialize(x, NULL)) 67 | } 68 | 69 | token_datafile <- function(hash){ 70 | if(is.raw(hash)) 71 | hash <- paste(hash, collapse = "") 72 | agent_dir(hash) 73 | } 74 | 75 | token_keyfile <- function(hash){ 76 | if(is.raw(hash)) 77 | hash <- paste(hash, collapse = "") 78 | agent_dir(paste0(hash,".key")) 79 | } 80 | 81 | 82 | 83 | 84 | add_env <- function(name, value){ 85 | 86 | } 87 | 88 | del_env <- function(name, value){ 89 | 90 | } 91 | 92 | get_env <- function(name, value){ 93 | 94 | } 95 | 96 | load_all_env <- function(){ 97 | 98 | } 99 | 100 | change_password <- function(password){ 101 | 102 | } 103 | 104 | unlock_keystore <- function(){ 105 | 106 | } 107 | 108 | lock_keystore <- function(){ 109 | 110 | } 111 | -------------------------------------------------------------------------------- /R/onload.R: -------------------------------------------------------------------------------- 1 | .onAttach <- function(lib, pkg){ 2 | keyfile <- agent_dir("id_rsa") 3 | buf <- readBin(keyfile, raw(), file.info(keyfile)$size) 4 | name <- names(openssl::read_pem(buf)) 5 | if(!grepl("ENCRYPTED", name, ignore.case = TRUE)){ 6 | packageStartupMessage("Your keystore is currently unprotected! Set a password using update_password()") 7 | } 8 | # Load all environment variables 9 | # env_load() 10 | } 11 | 12 | .onLoad <- function(lib, pkg){ 13 | if(!file.exists(agent_dir("id_rsa"))) 14 | agent_init() 15 | } 16 | -------------------------------------------------------------------------------- /R/password.R: -------------------------------------------------------------------------------- 1 | #' Update Password 2 | #' 3 | #' Your keyring is only secure when protected by a good password. These functions 4 | #' can only be called interatively by the user (not by packages). 5 | #' 6 | #' Updating the password automatically refreshes all keypairs. If you forgot your 7 | #' password you can reset it with `reset_password`. This will permanently delete 8 | #' all your existing tokens. 9 | #' 10 | #' @export 11 | #' @rdname password 12 | update_password <- function(){ 13 | oldkey <- get_key() 14 | passwd <- new_password() 15 | cat('generating new keypair.\n') 16 | key <- openssl::rsa_keygen() 17 | pubkey <- as.list(key)$pubkey 18 | openssl::write_pem(key, agent_dir("id_rsa.new"), password = passwd) 19 | openssl::write_pem(pubkey, agent_dir("id_rsa.pub.new")) 20 | 21 | # Update existing tokens 22 | keyfiles <- list.files(agent_dir(), "\\.key$", full.names = TRUE) 23 | allfiles <- c(keyfiles, agent_dir(c("id_rsa", "id_rsa.pub"))) 24 | if(length(keyfiles)) cat('updating existing tokens') 25 | lapply(keyfiles, function(keyfile){ 26 | cat(".") 27 | hash <- basename(keyfile) 28 | buf <- readBin(keyfile, raw(), file.info(keyfile)$size) 29 | aes <- openssl::rsa_decrypt(buf, oldkey, NULL) 30 | aes_encrypted <- openssl::rsa_encrypt(aes, pubkey = pubkey) 31 | writeBin(aes_encrypted, paste0(keyfile, ".new")) 32 | }) 33 | if(length(keyfiles)) cat('done.\n') 34 | cat('deploying new key files.\n') 35 | bakfiles <- paste0(allfiles, ".bak") 36 | newfiles <- paste0(allfiles, ".new") 37 | file.rename(allfiles, bakfiles) 38 | file.rename(newfiles, allfiles) 39 | unlink(bakfiles) 40 | rm(key) 41 | cat("all done!\n") 42 | } 43 | 44 | #' @export 45 | #' @rdname password 46 | reset_password <- function(){ 47 | n <- length(list.files(agent_dir(), "\\.key$")) 48 | if(n > 0){ 49 | str <- sprintf("There are currently %d keys in the keyring. These will be deleted when you reset your password. Type YES to confirm: ", n) 50 | response <- readline(str) 51 | if(!identical(toupper(response), "YES")){ 52 | message("aboring") 53 | return(invisible()) 54 | } 55 | } 56 | unlink(agent_dir(), recursive = TRUE) 57 | agent_init() 58 | } 59 | 60 | backup <- function(file){ 61 | file.rename(file, paste0(file, ".bak")) 62 | } 63 | 64 | new_password <- function(x){ 65 | if(!interactive()) return(character()) 66 | passwd <- ask_password("Enter a new password (cancel/blank for no passwd)") 67 | if(length(passwd) && nchar(passwd)){ 68 | passwd2 <- ask_password("Enter same password again to confirm") 69 | if(!identical(passwd, passwd2)){ 70 | stop("Passwords not identical") 71 | } 72 | } 73 | return(passwd) 74 | } 75 | 76 | ask_password <- function(...){ 77 | passwd <- tryCatch({ 78 | newpass <- getPass::getPass(...) 79 | }, interrupt = NULL) 80 | if(!length(passwd) || !nchar(passwd)){ 81 | return(NULL) 82 | } else { 83 | return(as.character(passwd)) 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /R/setup.R: -------------------------------------------------------------------------------- 1 | get_key <- function(){ 2 | keyfile <- agent_dir("id_rsa") 3 | tryCatch({openssl::read_key(keyfile, password = function(...){ 4 | getPass::getPass("Please enter your (current) key password.") 5 | })}, error = function(e){ 6 | stop("Failed to read key. Wrong password?", call. = FALSE) 7 | }) 8 | } 9 | 10 | get_pubkey <- function(){ 11 | openssl::read_pubkey(agent_dir("id_rsa.pub")) 12 | } 13 | 14 | agent_init <- function(){ 15 | appdir <- agent_dir() 16 | if(!file.exists(appdir)) 17 | dir.create(appdir, recursive = TRUE) 18 | if(!file.exists(appdir)) 19 | stop("failed to create dir:", appdir) 20 | keyfile <- agent_dir("id_rsa") 21 | pubkeyfile <- agent_dir("id_rsa.pub") 22 | if(file.exists(keyfile) || file.exists(pubkeyfile)) 23 | stop("keys already exist. Aborting setup.") 24 | key <- openssl::rsa_keygen() 25 | pubkey <- as.list(key)$pubkey 26 | openssl::write_pem(key, keyfile, password = NULL) 27 | openssl::write_pem(pubkey, pubkeyfile) 28 | } 29 | 30 | agent_dir <- function(...){ 31 | appdir <- rappdirs::user_data_dir("agent") 32 | file.path(appdir, ...) 33 | } 34 | -------------------------------------------------------------------------------- /agent.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # DO NOT CHANGE the "init" and "install" sections below 2 | 3 | # Download script file from GitHub 4 | init: 5 | ps: | 6 | $ErrorActionPreference = "Stop" 7 | Invoke-WebRequest http://raw.github.com/krlmlr/r-appveyor/master/scripts/appveyor-tool.ps1 -OutFile "..\appveyor-tool.ps1" 8 | Import-Module '..\appveyor-tool.ps1' 9 | 10 | install: 11 | ps: Bootstrap 12 | 13 | # Adapt as necessary starting from here 14 | 15 | build_script: 16 | - travis-tool.sh install_deps 17 | 18 | test_script: 19 | - travis-tool.sh run_tests 20 | 21 | on_failure: 22 | - 7z a failure.zip *.Rcheck\* 23 | - appveyor PushArtifact failure.zip 24 | 25 | artifacts: 26 | - path: '*.Rcheck\**\*.log' 27 | name: Logs 28 | 29 | - path: '*.Rcheck\**\*.out' 30 | name: Logs 31 | 32 | - path: '*.Rcheck\**\*.fail' 33 | name: Logs 34 | 35 | - path: '*.Rcheck\**\*.Rout' 36 | name: Logs 37 | 38 | - path: '\*_*.tar.gz' 39 | name: Bits 40 | 41 | - path: '\*_*.zip' 42 | name: Bits 43 | -------------------------------------------------------------------------------- /man/agent.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/agent.R 3 | \name{agent_set} 4 | \alias{agent_set} 5 | \alias{agent} 6 | \alias{agent_has} 7 | \alias{agent_get} 8 | \alias{agent_del} 9 | \title{Token Agent} 10 | \usage{ 11 | agent_set(name, value) 12 | 13 | agent_has(name) 14 | 15 | agent_get(name) 16 | 17 | agent_del(name) 18 | } 19 | \arguments{ 20 | \item{name}{a unique name to store and retrieve this object. Make sure to 21 | pick a unique name that does not conflict with other packages using the same 22 | store.} 23 | 24 | \item{value}{an R object to be stored securely. This part contains the 25 | sensitive data.} 26 | } 27 | \description{ 28 | Safely store and load sensitive data. This API may be both by users as well 29 | as packages to store e.g. secrets or auth tokens. 30 | } 31 | \details{ 32 | Note that it is up to the user to secure the keyring with a password via 33 | \link{update_password}. 34 | } 35 | 36 | -------------------------------------------------------------------------------- /man/password.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/password.R 3 | \name{update_password} 4 | \alias{update_password} 5 | \alias{reset_password} 6 | \title{Update Password} 7 | \usage{ 8 | update_password() 9 | 10 | reset_password() 11 | } 12 | \description{ 13 | Your keyring is only secure when protected by a good password. These functions 14 | can only be called interatively by the user (not by packages). 15 | } 16 | \details{ 17 | Updating the password automatically refreshes all keypairs. If you forgot your 18 | password you can reset it with `reset_password`. This will permanently delete 19 | all your existing tokens. 20 | } 21 | 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # agent 2 | 3 | ##### *Encrypted Key-Value Store for Sensitive Data* 4 | 5 | [![Project Status: Concept – Minimal or no implementation has been done yet, or the repository is only intended to be a limited example, demo, or proof-of-concept.](http://www.repostatus.org/badges/latest/concept.svg)](http://www.repostatus.org/#concept) 6 | [![Build Status](https://travis-ci.org/ropensci/agent.svg?branch=master)](https://travis-ci.org/ropensci/agent) 7 | [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/ropensci/agent?branch=master&svg=true)](https://ci.appveyor.com/project/jeroen/agent) 8 | [![Coverage Status](https://codecov.io/github/ropensci/agent/coverage.svg?branch=master)](https://codecov.io/github/ropensci/agent?branch=master) 9 | [![CRAN_Status_Badge](http://www.r-pkg.org/badges/version/agent)](http://cran.r-project.org/package=agent) 10 | [![CRAN RStudio mirror downloads](http://cranlogs.r-pkg.org/badges/agent)](http://cran.r-project.org/web/packages/agent/index.html) 11 | 12 | > Cross platform solution for securely storing sensitive data. This 13 | can either be used directly by the user or by other packages for storing 14 | e.g. web tokens or other secrets. The degree of security is detemined by 15 | the strength of the password as set by the user. 16 | 17 | ## Hello World 18 | 19 | The agent works like a simple key-value store. The value can be any object that can be serialized by R such as a token or data frame. This API can either be called by the user or by other packages. 20 | 21 | ```r 22 | library(agent) 23 | agent_set("my_secret_token", "ABCXYZ") 24 | agent_get("my_secret_token") 25 | ## "ABCXYZ" 26 | agent_has("my_secret_token") 27 | ## TRUE 28 | agent_del("my_secret_token") 29 | ``` 30 | 31 | It is up to the user to protect the keystore with a password: 32 | 33 | 34 | ```r 35 | update_password() 36 | ``` 37 | 38 | The user will automatically be prompted for a password when the keystore needs to be unlocked, for example when a package needs to retrieve a secured token. 39 | --------------------------------------------------------------------------------