Skip to content

Commit

Permalink
Add key_list_raw() method to return keys as raw vectors
Browse files Browse the repository at this point in the history
For the case when they contain zero bytes. In this case the
`service` and/or `username` is `NA`.

Closes #158.
  • Loading branch information
gaborcsardi committed Dec 29, 2024
1 parent cefb72c commit 99b469e
Show file tree
Hide file tree
Showing 13 changed files with 346 additions and 21 deletions.
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Config/Needs/website: tidyverse/tidytemplate
Config/testthat/edition: 3
Encoding: UTF-8
Roxygen: list(markdown = TRUE, r6 = FALSE)
RoxygenNote: 7.2.3
RoxygenNote: 7.3.2
SystemRequirements: Optional: libsecret on Linux (libsecret-1-dev on
Debian/Ubuntu, libsecret-devel on Fedora/CentOS)
Collate:
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export(key_delete)
export(key_get)
export(key_get_raw)
export(key_list)
export(key_list_raw)
export(key_set)
export(key_set_with_raw_value)
export(key_set_with_value)
Expand Down
23 changes: 22 additions & 1 deletion R/api.R
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
#' `key_list` lists all keys of a keyring, or the keys for a certain
#' service (if `service` is not `NULL`).
#'
#' `key_list_raw()` is like `key_list()` but also returns the keys as raw
#' values. This is useful if your keys have bytes that cannot appear
#' in R strings, e.g. a zero byte.
#'
#' ## Encodings
#'
#' On Windows, if required, an encoding can be specified using either
Expand Down Expand Up @@ -56,7 +60,16 @@
#' confidential information that was stored in the key.
#'
#' `key_list` returns a list of keys, i.e. service names and usernames,
#' in a data frame.
#' in a data frame with column names `service` and `username`. If a
#' service or user name contains a zero byte, which is not allowed in an
#' R string, that entry is shown as `NA` and a warning (of class
#' `keyring_warn_zero_byte_keys`) is thrown. You can use the
#' `key_list_raw()` function to query these keys.
#'
#' `key_list_raw` is similar to `key_list` but returns service and
#' usernames as raw vectors. This is useful if some service or user
#' names) contain zero bytes. All column names: `service`, `username`,
#' `service_raw`, `username_raw`.

Check warning on line 72 in R/api.R

View check run for this annotation

Codecov / codecov/patch

R/api.R#L63-L72

Added lines #L63 - L72 were not covered by tests
#'
#' @export
#' @examples
Expand Down Expand Up @@ -163,6 +176,14 @@ key_list <- function(service = NULL, keyring = NULL) {
default_backend()$list(service, keyring = keyring)
}

#' @export
#' @rdname key_get

key_list_raw <- function(service = NULL, keyring = NULL) {
assert_that(is_non_empty_string_or_null(service))
default_backend()$list_raw(service, keyring = keyring)

Check warning on line 184 in R/api.R

View check run for this annotation

Codecov / codecov/patch

R/api.R#L183-L184

Added lines #L183 - L184 were not covered by tests
}

#' Operations on keyrings
#'
#' On most platforms `keyring` supports multiple keyrings. This includes
Expand Down
9 changes: 9 additions & 0 deletions R/backend-class.R
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ abstract_method <- function() {
#' keyring = NULL)
#' delete(service, username = NULL, keyring = NULL)
#' list(service = NULL, keyring = NULL)
#' list_raw(service = NULL, keyring = NULL)
#' ```
#'
#' What these functions do:
Expand All @@ -39,6 +40,7 @@ abstract_method <- function() {
#' byte sequence of a raw vector.
#' * `delete()` remotes a keyring item.
#' * `list()` lists keyring items.
#' * `list_raw()` lists keyring items, also as raw vectors.
#'
#' The arguments:
#' * `service` String, the name of a service. This is used to find the
Expand Down Expand Up @@ -83,6 +85,12 @@ backend <- R6Class(
abstract_method(),
list = function(service = NULL, keyring = NULL)
stop("Backend does not implement 'list'"),
list_raw = function(service = NULL, keyring = NULL) {
keys <- self$list(service, keyring)
keys$service_raw <- lapply(keys$service, charToRaw)
keys$username_raw <- lapply(keys$username, charToRaw)
keys

Check warning on line 92 in R/backend-class.R

View check run for this annotation

Codecov / codecov/patch

R/backend-class.R#L88-L92

Added lines #L88 - L92 were not covered by tests
},

print = function(...) {
d <- self$docs()
Expand Down Expand Up @@ -116,6 +124,7 @@ backend <- R6Class(
set_with_value = "set a key in the keyring",
delete = "delete a key",
list = "list keys in a keyring",
list_raw = "list keys in a keyring as raw vectors",

Check warning on line 127 in R/backend-class.R

View check run for this annotation

Codecov / codecov/patch

R/backend-class.R#L127

Added line #L127 was not covered by tests
has_keyring_support = "TRUE if multiple keyrings are supported"
)
}
Expand Down
34 changes: 33 additions & 1 deletion R/backend-macos.R
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ backend_macos <- R6Class(
b_macos_delete(self, private, service, username, keyring),
list = function(service = NULL, keyring = NULL)
b_macos_list(self, private, service, keyring),
list_raw = function(service = NULL, keyring = NULL)
b_macos_list_raw(self, private, service, keyring),

keyring_create = function(keyring, password = NULL)
b_macos_keyring_create(self, private, keyring, password),
Expand Down Expand Up @@ -143,11 +145,41 @@ b_macos_delete <- function(self, private, service, username, keyring) {
b_macos_list <- function(self, private, service, keyring) {
keyring <- private$keyring_file(keyring %||% private$keyring)
res <- .Call(keyring_macos_list, utf8(keyring), utf8(service))
data.frame(
df <- data.frame(
service = res[[1]],
username = res[[2]],
stringsAsFactors = FALSE
)
srv_na <- anyNA(df[["service"]])
usr_na <- anyNA(df[["username"]])
if (srv_na | usr_na) {
cnd <- structure(
list(message = paste0(
"Some ",
if (srv_na) "service names ",
if (srv_na && usr_na) "and some ",
if (usr_na) "user names ",
"contain zero bytes. These are shown as NA. ",
"Use `key_list_raw()` to see them."
)),
class = "keyring_warn_zero_byte_keys"
)
warning(cnd)
}

Check warning on line 168 in R/backend-macos.R

View check run for this annotation

Codecov / codecov/patch

R/backend-macos.R#L168

Added line #L168 was not covered by tests
df
}

b_macos_list_raw <- function(self, private, service, keyring) {

Check warning on line 172 in R/backend-macos.R

View check run for this annotation

Codecov / codecov/patch

R/backend-macos.R#L171-L172

Added lines #L171 - L172 were not covered by tests
keyring <- private$keyring_file(keyring %||% private$keyring)
res <- .Call(keyring_macos_list, utf8(keyring), utf8(service))
df <- data.frame(
service = res[[1]],
username = res[[2]],
stringsAsFactors = FALSE
)
df[["service_raw"]] <- res[[3]]
df[["username_raw"]] <- res[[4]]
df
}

b_macos_keyring_create <- function(self, private, keyring, password) {
Expand Down
10 changes: 5 additions & 5 deletions R/package.R
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@
#' the termination of and R session. Specifically, you can define a key
#' once, and then read the key value in completely independent R sessions.
#'
#' - Setting a secret interactively: [key_set()]
#' - Setting a secret interactively: [key_set()].
#' - Setting a secret from a script, i.e. non-interactively:
#' [key_set_with_value()]
#' - Reading a secret: [key_get()]
#' - Listing secrets: [key_list()]
#' - Deleting a secret: [key_delete()]
#' [key_set_with_value()].
#' - Reading a secret: [key_get()], [key_get_raw()].
#' - Listing secrets: [key_list()], [key_list_raw()].
#' - Deleting a secret: [key_delete()].
#'
#' @section Managing keyrings:
#'
Expand Down
2 changes: 2 additions & 0 deletions man/backend.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 17 additions & 1 deletion man/key_get.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 5 additions & 6 deletions man/keyring-package.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 30 additions & 5 deletions src/keyring_macos.c
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,33 @@ static void keyring_macos_list_item(SecKeychainItemRef item, SEXP result,
/* outData = */ NULL);
# pragma GCC diagnostic pop
keyring_macos_handle_status("cannot list passwords", status);
SET_STRING_ELT(VECTOR_ELT(result, 0), idx,
mkCharLen(attrs[0].data, attrs[0].length));
SET_STRING_ELT(VECTOR_ELT(result, 1), idx,
mkCharLen(attrs[1].data, attrs[1].length));
SET_VECTOR_ELT(VECTOR_ELT(result, 2), idx,
Rf_allocVector(RAWSXP, attrs[0].length));
memcpy(
RAW(VECTOR_ELT(VECTOR_ELT(result, 2), idx)),
attrs[0].data,
attrs[0].length
);
if (memchr(attrs[0].data, '\0', attrs[0].length) == NULL) {
SET_STRING_ELT(VECTOR_ELT(result, 0), idx,
mkCharLen(attrs[0].data, attrs[0].length));
} else {
SET_STRING_ELT(VECTOR_ELT(result, 0), idx, NA_STRING);
}

SET_VECTOR_ELT(VECTOR_ELT(result, 3), idx,
Rf_allocVector(RAWSXP, attrs[1].length));
memcpy(
RAW(VECTOR_ELT(VECTOR_ELT(result, 3), idx)),
attrs[1].data,
attrs[1].length
);
if (memchr(attrs[0].data, '\0', attrs[0].length) == NULL) {
SET_STRING_ELT(VECTOR_ELT(result, 1), idx,
mkCharLen(attrs[1].data, attrs[1].length));
} else {
SET_STRING_ELT(VECTOR_ELT(result, 1), idx, NA_STRING);
}
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
SecKeychainItemFreeContent(&attrList, NULL);
Expand Down Expand Up @@ -287,9 +310,11 @@ SEXP keyring_macos_list(SEXP keyring, SEXP service) {
CFArrayRef resArray = keyring_macos_list_get(ckeyring, cservice);
CFIndex i, num = CFArrayGetCount(resArray);
SEXP result;
PROTECT(result = allocVector(VECSXP, 2));
PROTECT(result = allocVector(VECSXP, 4));
SET_VECTOR_ELT(result, 0, allocVector(STRSXP, num));
SET_VECTOR_ELT(result, 1, allocVector(STRSXP, num));
SET_VECTOR_ELT(result, 2, allocVector(VECSXP, num));
SET_VECTOR_ELT(result, 3, allocVector(VECSXP, num));
for (i = 0; i < num; i++) {
SecKeychainItemRef item =
(SecKeychainItemRef) CFArrayGetValueAtIndex(resArray, i);
Expand Down
54 changes: 54 additions & 0 deletions tests/testthat/_snaps/macos.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# zero bytes in keys

Code
b_macos_list(NULL, list(keyring_file = function(...) NULL))
Condition
Warning in `b_macos_list()`:
Some service names contain zero bytes. These are shown as NA. Use `key_list_raw()` to see them.
Output
service username
1 foo bar
2 <NA> baz
Code
b_macos_list_raw(NULL, list(keyring_file = function(...) NULL))
Output
service username service_raw username_raw
1 foo bar 66, 6f, 6f 62, 61, 72
2 <NA> baz 03, 02, 01, 00 62, 61, 7a

---

Code
b_macos_list(NULL, list(keyring_file = function(...) NULL))
Condition
Warning in `b_macos_list()`:
Some service names and some user names contain zero bytes. These are shown as NA. Use `key_list_raw()` to see them.
Output
service username
1 foo bar
2 <NA> <NA>
Code
b_macos_list_raw(NULL, list(keyring_file = function(...) NULL))
Output
service username service_raw username_raw
1 foo bar 66, 6f, 6f 62, 61, 72
2 <NA> <NA> 03, 02, 01, 00 01, 02, 00, 01, 02

---

Code
b_macos_list(NULL, list(keyring_file = function(...) NULL))
Condition
Warning in `b_macos_list()`:
Some user names contain zero bytes. These are shown as NA. Use `key_list_raw()` to see them.
Output
service username
1 foo bar
2 baz <NA>
Code
b_macos_list_raw(NULL, list(keyring_file = function(...) NULL))
Output
service username service_raw username_raw
1 foo bar 66, 6f, 6f 62, 61, 72
2 baz <NA> 62, 61, 7a 03, 02, 01, 00

Loading

0 comments on commit 99b469e

Please sign in to comment.