├── .Rbuildignore ├── .github ├── .gitignore └── workflows │ └── R-CMD-check.yaml ├── .gitignore ├── DESCRIPTION ├── LICENSE ├── NAMESPACE ├── NEWS ├── R ├── base64url.R ├── claim.R ├── jwt.R ├── read.R └── write.R ├── appveyor.yml ├── inst └── WORDLIST ├── jose.Rproj ├── man ├── base64url_encode.Rd ├── jwk.Rd ├── jwt_claim.Rd └── jwt_encode.Rd ├── readme.md ├── tests ├── js │ ├── aes.js │ ├── ec.js │ ├── hmac.js │ └── rsa.js ├── keys │ ├── aes_cbc.bin │ ├── aes_cbc.json │ ├── aes_ctr.bin │ ├── aes_ctr.json │ ├── aes_gcm.bin │ ├── aes_gcm.json │ ├── data │ ├── ecdh.bin │ ├── ecdh.json │ ├── ecdh.pub.json │ ├── ecdsa.json │ ├── ecdsa.pub.json │ ├── ecdsa.sig │ ├── hmac.json │ ├── hmac.sig │ ├── rsa-oaep.bin │ ├── rsa-oaep.json │ ├── rsa-oaep.pub.json │ ├── rsa-pkcs1.json │ ├── rsa-pkcs1.pub.json │ └── rsa-pkcs1.sig ├── spelling.R ├── testthat.R └── testthat │ ├── test_aes.R │ ├── test_claims.R │ ├── test_ec.R │ ├── test_examples.R │ ├── test_exp.R │ ├── test_header.R │ ├── test_hmac.R │ ├── test_rsa.R │ └── test_sizes.R └── vignettes ├── jwk.Rmd └── jwt.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^\.travis\.yml$ 4 | ^readme.md$ 5 | ^appveyor\.yml$ 6 | ^\.github$ 7 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | pull_request: 6 | 7 | name: R-CMD-check.yaml 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | R-CMD-check: 13 | runs-on: ${{ matrix.config.os }} 14 | 15 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | config: 21 | - {os: macos-13, r: 'release'} 22 | - {os: macos-14, r: 'release'} 23 | - {os: windows-latest, r: '4.1'} 24 | - {os: windows-latest, r: '4.2'} 25 | - {os: windows-latest, r: 'release'} 26 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 27 | - {os: ubuntu-latest, r: 'release'} 28 | - {os: ubuntu-latest, r: 'oldrel-1'} 29 | 30 | env: 31 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 32 | R_KEEP_PKG_SOURCE: yes 33 | 34 | steps: 35 | - uses: actions/checkout@v4 36 | 37 | - uses: r-lib/actions/setup-pandoc@v2 38 | 39 | - uses: r-lib/actions/setup-r@v2 40 | with: 41 | r-version: ${{ matrix.config.r }} 42 | http-user-agent: ${{ matrix.config.http-user-agent }} 43 | use-public-rspm: true 44 | 45 | - uses: r-lib/actions/setup-r-dependencies@v2 46 | with: 47 | extra-packages: any::rcmdcheck 48 | needs: check 49 | 50 | - uses: r-lib/actions/check-r-package@v2 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | .Rproj.user 4 | .Rhistory 5 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: jose 2 | Type: Package 3 | Title: JavaScript Object Signing and Encryption 4 | Version: 1.2.1 5 | Authors@R: person("Jeroen", "Ooms", role = c("aut", "cre"), 6 | email = "jeroenooms@gmail.com", comment = c(ORCID = "0000-0002-4035-0289")) 7 | Description: Read and write JSON Web Keys (JWK, rfc7517), generate and verify JSON 8 | Web Signatures (JWS, rfc7515) and encode/decode JSON Web Tokens (JWT, rfc7519) 9 | . These standards provide 10 | modern signing and encryption formats that are natively supported by browsers 11 | via the JavaScript WebCryptoAPI , 12 | and used by services like OAuth 2.0, LetsEncrypt, and Github Apps. 13 | License: MIT + file LICENSE 14 | URL: https://r-lib.r-universe.dev/jose 15 | BugReports: https://github.com/r-lib/jose/issues 16 | Depends: openssl (>= 1.2.1) 17 | Imports: jsonlite 18 | RoxygenNote: 7.1.2 19 | VignetteBuilder: knitr 20 | Suggests: 21 | spelling, 22 | testthat, 23 | knitr, 24 | rmarkdown 25 | Encoding: UTF-8 26 | Language: en-US 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2019 2 | COPYRIGHT HOLDER: Jeroen Ooms 3 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(print,jwt_claim) 4 | export(base64url_decode) 5 | export(base64url_encode) 6 | export(jwk_read) 7 | export(jwk_write) 8 | export(jwt_claim) 9 | export(jwt_decode_hmac) 10 | export(jwt_decode_sig) 11 | export(jwt_encode_hmac) 12 | export(jwt_encode_sig) 13 | export(jwt_split) 14 | export(read_jwk) 15 | export(write_jwk) 16 | importFrom(jsonlite,fromJSON) 17 | importFrom(jsonlite,toJSON) 18 | importFrom(jsonlite,validate) 19 | importFrom(openssl,base64_decode) 20 | importFrom(openssl,base64_encode) 21 | importFrom(openssl,bignum) 22 | importFrom(openssl,read_key) 23 | importFrom(openssl,read_pubkey) 24 | importFrom(openssl,sha2) 25 | importFrom(openssl,signature_create) 26 | importFrom(openssl,signature_verify) 27 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | 1.2 2 | - jwt_encode_sig() now allows to override the typ field via headers #15 3 | - jwt_decode functions now check the 'exp' and 'nbf' fields and raise 4 | and error if token has expired. 5 | 6 | 1.1 7 | - Allow for empty list attributes in jwt_claim(), issue #13 8 | 9 | 1.0 10 | - jwt_encode_sig() and jwt_decode_sig() now use proper 64bit signatures 11 | as described in the spec (instead of openssl DER structures) 12 | 13 | 0.2 14 | - jwt_encode_hmac() and jwt_encode_sig() gain a 'header' parameter 15 | - Add spell checker, update maintainer email address 16 | -------------------------------------------------------------------------------- /R/base64url.R: -------------------------------------------------------------------------------- 1 | #' Base64URL encoding 2 | #' 3 | #' The \code{base64url_encode} functions are a variant of the standard base64. They are 4 | #' specified in Section 5 of RFC 4648 as a URL-safe alternative. They use different symbols 5 | #' for the 62:nd and 63:rd alphabet character and do not include trailing \code{==} 6 | #' padding. 7 | #' 8 | #' @rdname base64url_encode 9 | #' @importFrom openssl base64_encode base64_decode 10 | #' @param text a base64url encoded string 11 | #' @param bin a binary blob to encode 12 | #' @export 13 | base64url_encode <- function(bin){ 14 | text <- base64_encode(bin) 15 | sub("=+$", "", chartr('+/', '-_', text)) 16 | } 17 | 18 | #' @rdname base64url_encode 19 | #' @importFrom openssl base64_decode 20 | #' @export 21 | base64url_decode <- function(text){ 22 | text <- fix_padding(chartr('-_', '+/', text)) 23 | base64_decode(text) 24 | } 25 | 26 | # Ensures base64 length is a multiple of 4 27 | fix_padding <- function(text){ 28 | text <- gsub("[\r\n]", "", text)[[1]] 29 | mod <- nchar(text) %% 4; 30 | if(mod > 0){ 31 | padding <- paste(rep("=", (4 - mod)), collapse = "") 32 | text <- paste0(text, padding) 33 | } 34 | text 35 | } 36 | -------------------------------------------------------------------------------- /R/claim.R: -------------------------------------------------------------------------------- 1 | #' Generate claim 2 | #' 3 | #' Helper function to create a named list used as the claim of a JWT payload. 4 | #' See \url{https://tools.ietf.org/html/rfc7519#section-4.1} for details. 5 | #' 6 | #' @export 7 | #' @param iss (Issuer) Claim, should be rfc7519 'StringOrURI' value 8 | #' @param sub (Subject) Claim, should be rfc7519 'StringOrURI' value 9 | #' @param aud (Audience) Claim, should contain one or rfc7519 'StringOrURI' values 10 | #' @param exp (Expiration Time) Claim, should be rfc7519 'NumericDate' value; R 11 | #' \code{POSIXct} values are automatically coerced. 12 | #' @param nbf (Not Before) Claim, should be rfc7519 'NumericDate' value; R 13 | #' \code{POSIXct} values are automatically coerced. 14 | #' @param iat (Issued At) Claim, should be rfc7519 'NumericDate' value; R 15 | #' \code{POSIXct} values are automatically coerced. 16 | #' @param jti (JWT ID) Claim, optional unique identifier for the JWT 17 | #' @param ... additional custom claims to include 18 | jwt_claim <- function(iss = NULL, sub = NULL, aud = NULL, exp = NULL, nbf = NULL, 19 | iat = Sys.time(), jti = NULL, ...){ 20 | values <- list( 21 | iss = validate_stringoruri(iss), 22 | sub = validate_stringoruri(sub), 23 | aud = validate_stringoruri(aud), 24 | exp = validate_numericdate(exp), 25 | nbf = validate_numericdate(nbf), 26 | iat = validate_numericdate(iat), 27 | jti = jti, 28 | ... 29 | ) 30 | structure(Filter(function(x){is.list(x) || length(x)}, values), class = c("jwt_claim", "list")) 31 | } 32 | 33 | #' @export 34 | print.jwt_claim <- function(x, ...){ 35 | print(unclass(x)) 36 | } 37 | 38 | validate_stringoruri <- function(str){ 39 | if(is.null(str)) return(NULL) 40 | if(!is.character(str)) 41 | stop("Invalid 'StringOrURI' value: ", str) 42 | if(any(grepl(":", str, fixed = TRUE) & !grepl("[a-z]+://", str))) 43 | stop("Invalid 'StringOrURI' value, the ':' may only appear within a URL") 44 | str 45 | } 46 | 47 | validate_numericdate <- function(val){ 48 | if(is.null(val)) return(NULL) 49 | if(inherits(val, 'POSIXt')) 50 | val <- unclass(as.POSIXct(val)) 51 | max <- unclass(as.POSIXct("2200-01-01")) 52 | if(!is.numeric(val) || length(val) > 1 || val > max) 53 | stop("Invalid 'NumericDate' (seconds since epoch) value: ", val) 54 | round(val) 55 | } 56 | -------------------------------------------------------------------------------- /R/jwt.R: -------------------------------------------------------------------------------- 1 | #' JSON Web Token 2 | #' 3 | #' Sign or verify a JSON web token. The \code{jwt_encode_hmac}, \code{jwt_encode_rsa}, 4 | #' and \code{jwt_encode_ec} default to \code{HS256}, \code{RS256}, and \code{ES256} 5 | #' respectively. See \href{https://jwt.io}{jwt.io} or 6 | #' \href{https://tools.ietf.org/html/rfc7519}{RFC7519} for more details. 7 | #' 8 | #' @export 9 | #' @rdname jwt_encode 10 | #' @aliases jwt jose 11 | #' @param claim a named list with fields to include in the jwt payload 12 | #' @param secret string or raw vector with a secret passphrase 13 | #' @param size bitsize of sha2 signature, i.e. \code{sha256}, \code{sha384} or \code{sha512}. 14 | #' Only for HMAC/RSA, not applicable for ECDSA keys. 15 | #' @param header named list with additional parameter fields to include in the jwt header as 16 | #' defined in \href{https://tools.ietf.org/html/rfc7515#section-9.1.2}{rfc7515 section 9.1.2} 17 | #' @param jwt string containing the JSON Web Token (JWT) 18 | #' @param key path or object with RSA or EC private key, see \link[openssl:read_key]{openssl::read_key}. 19 | #' @param pubkey path or object with RSA or EC public key, see \link[openssl:read_pubkey]{openssl::read_pubkey}. 20 | #' @importFrom openssl sha2 signature_create signature_verify read_pubkey read_key 21 | #' @importFrom jsonlite fromJSON toJSON 22 | #' @examples # HMAC signing 23 | #' mysecret <- "This is super secret" 24 | #' token <- jwt_claim(name = "jeroen", session = 123456) 25 | #' sig <- jwt_encode_hmac(token, mysecret) 26 | #' jwt_decode_hmac(sig, mysecret) 27 | #' 28 | #' # RSA encoding 29 | #' mykey <- openssl::rsa_keygen() 30 | #' pubkey <- as.list(mykey)$pubkey 31 | #' sig <- jwt_encode_sig(token, mykey) 32 | #' jwt_decode_sig(sig, pubkey) 33 | #' 34 | #' # Same with EC 35 | #' mykey <- openssl::ec_keygen() 36 | #' pubkey <- as.list(mykey)$pubkey 37 | #' sig <- jwt_encode_sig(token, mykey) 38 | #' jwt_decode_sig(sig, pubkey) 39 | #' 40 | #' # Get elements of the key 41 | #' mysecret <- "This is super secret" 42 | #' token <- jwt_claim(name = "jeroen", session = 123456) 43 | #' jwt <- jwt_encode_hmac(token, mysecret) 44 | #' jwt_split(jwt) 45 | jwt_encode_hmac <- function(claim = jwt_claim(), secret, size = 256, header = NULL) { 46 | stopifnot(inherits(claim, "jwt_claim")) 47 | if(is.character(secret)) 48 | secret <- charToRaw(secret) 49 | if(!is.raw(secret)) 50 | stop("Secret must be a string or raw vector") 51 | if(inherits(secret, "rsa") || inherits(secret, "dsa") || inherits(secret, "ecdsa")) 52 | stop("Secret must be raw bytes, not a: ", class(secret)[-1]) 53 | jwt_header <- to_json(c(list( 54 | typ = "JWT", 55 | alg = paste0("HS", size) 56 | ), header)) 57 | body <- to_json(claim) 58 | doc <- paste(base64url_encode(jwt_header), base64url_encode(body), sep = ".") 59 | sig <- sha2(charToRaw(doc), size = size, key = secret) 60 | paste(doc, base64url_encode(sig), sep = ".") 61 | } 62 | 63 | #' @export 64 | #' @rdname jwt_encode 65 | jwt_decode_hmac <- function(jwt, secret){ 66 | if(is.character(secret)) 67 | secret <- charToRaw(secret) 68 | if(!is.raw(secret)) 69 | stop("Secret must be a string or raw vector") 70 | if(inherits(secret, "rsa") || inherits(secret, "dsa") || inherits(secret, "ecdsa")) 71 | stop("Secret must be raw bytes, not a: ", class(secret)[-1]) 72 | out <- jwt_split(jwt) 73 | if(out$type != "HMAC") 74 | stop("Invalid algorithm: ", out$type) 75 | sig <- sha2(out$data, size = out$keysize, key = secret) 76 | if(!identical(out$sig, unclass(sig))) 77 | stop("HMAC signature verification failed!", call. = FALSE) 78 | check_expiration_time(out$payload) 79 | structure(out$payload, class = c("jwt_claim", "list")) 80 | } 81 | 82 | #' @export 83 | #' @rdname jwt_encode 84 | jwt_encode_sig <- function(claim = jwt_claim(), key, size = 256, header = NULL) { 85 | stopifnot(inherits(claim, "jwt_claim")) 86 | key <- read_key(key) 87 | if(!inherits(key, "key")) 88 | stop("key must be rsa/ecdsa private key") 89 | # See http://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-3.4 90 | jwt_header <- if(inherits(key, "rsa")){ 91 | if(as.list(key)$size < 2048) 92 | stop("RSA keysize must be at least 2048 bit") 93 | to_json(utils::modifyList(list( 94 | typ = "JWT", 95 | alg = paste0("RS", size) 96 | ), as.list(header))) 97 | } else if(inherits(key, "ecdsa")){ 98 | # See http://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-3.4 99 | size <- switch(as.list(key)$data$curve, 100 | "P-256" = 256, "P-384" = 384, "P-521" = 512, stop("invalid curve")) 101 | to_json(utils::modifyList(list( 102 | typ = "JWT", 103 | alg = paste0("ES", size) 104 | ), as.list(header))) 105 | } else { 106 | stop("Key must be RSA or ECDSA private key") 107 | } 108 | doc <- paste(base64url_encode(jwt_header), base64url_encode(to_json(claim)), sep = ".") 109 | dgst <- sha2(charToRaw(doc), size = size) 110 | sig <- signature_create(dgst, hash = NULL, key = key) 111 | if(inherits(key, "ecdsa")){ 112 | params <- openssl::ecdsa_parse(sig) 113 | bitsize <- ceiling(size / 8) 114 | sig <- c(pad_bignum(params$r, size), pad_bignum(params$s, size)) 115 | } 116 | paste(doc, base64url_encode(sig), sep = ".") 117 | } 118 | 119 | #' @export 120 | #' @rdname jwt_encode 121 | jwt_decode_sig <- function(jwt, pubkey){ 122 | out <- jwt_split(jwt) 123 | if(out$type != "RSA" && out$type != "ECDSA") 124 | stop("Invalid algorithm: ", out$type) 125 | key <- read_pubkey(pubkey) 126 | if((!inherits(key, "rsa") && !inherits(key, "ecdsa")) || !inherits(key, "pubkey")) 127 | stop("Key must be rsa/ecdsa public key") 128 | dgst <- sha2(out$data, size = out$keysize) 129 | if(out$type == "ECDSA"){ 130 | bitsize <- length(out$sig)/2 131 | r <- out$sig[seq_len(bitsize)] 132 | s <- out$sig[seq_len(bitsize) + bitsize] 133 | out$sig <- openssl::ecdsa_write(r, s) 134 | } 135 | if(!signature_verify(dgst, out$sig, hash = NULL, pubkey = key)) 136 | stop(out$type, " signature verification failed!", call. = FALSE) 137 | check_expiration_time(out$payload) 138 | structure(out$payload, class = c("jwt_claim", "list")) 139 | } 140 | 141 | #' @export 142 | #' @rdname jwt_encode 143 | jwt_split <- function(jwt){ 144 | input <- strsplit(jwt, ".", fixed = TRUE)[[1]] 145 | stopifnot(length(input) %in% c(2,3)) 146 | header <- fromJSON(rawToChar(base64url_decode(input[1]))) 147 | stopifnot(toupper(header$typ) == "JWT") 148 | if(is.na(input[3])) input[3] = "" 149 | sig <- base64url_decode(input[3]) 150 | payload <- fromJSON(rawToChar(base64url_decode(input[2]))) 151 | data <- charToRaw(paste(input[1:2], collapse = ".")) 152 | if(!grepl("^none|[HRE]S(256|384|512)$", header$alg)) 153 | stop("Invalid algorithm: ", header$alg) 154 | keysize <- as.numeric(substring(header$alg, 3)) 155 | type <- match.arg(substring(header$alg, 1, 1), c("HMAC", "RSA", "ECDSA")) 156 | list(type = type, keysize = keysize, data = data, sig = sig, payload = payload, header = header) 157 | } 158 | 159 | to_json <- function(x){ 160 | jsonlite::toJSON(x, auto_unbox = TRUE) 161 | } 162 | 163 | # Adds leading zeros if needed (P512 is 521 bit == 66 bytes) 164 | # Spec: https://tools.ietf.org/html/rfc7518#page-10 165 | pad_bignum <- function(x, keysize){ 166 | stopifnot(keysize %in% c(256, 384, 512)) 167 | bitsize <- switch (as.character(keysize), "256" = 32, "384" = 48, "512" = 66) 168 | c(raw(bitsize - length(x)), x) 169 | } 170 | 171 | # As suggested in the spec, we give a 60s grace period to account 172 | # for inaccurate clocks. 173 | check_expiration_time <- function(payload){ 174 | if(length(payload$exp)){ 175 | stopifnot("exp claim is a number" = is.numeric(payload$exp)) 176 | expdate <- structure(payload$exp, class = c("POSIXct", "POSIXt")) 177 | if(expdate < (Sys.time() - 60)){ 178 | stop(paste("Token has expired on", expdate), call. = FALSE) 179 | } 180 | } 181 | if(length(payload$nbf)){ 182 | stopifnot("nbf claim is a number" = is.numeric(payload$nbf)) 183 | nbfdate <- structure(payload$nbf, class = c("POSIXct", "POSIXt")) 184 | if(nbfdate > (Sys.time() + 60)){ 185 | stop(paste("Token is not valid before", nbfdate), call. = FALSE) 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /R/read.R: -------------------------------------------------------------------------------- 1 | #' @rdname jwk 2 | #' @aliases jwk_read 3 | #' @param file path to file with key data or literal json string 4 | #' @importFrom jsonlite fromJSON validate 5 | #' @export 6 | read_jwk <- function(file){ 7 | jwk <- if(is.character(file)){ 8 | if(validate(file)){ 9 | fromJSON(file) 10 | } else { 11 | fromJSON(rawToChar(openssl:::read_input(file))) 12 | } 13 | } else { 14 | file 15 | } 16 | if(!is.list(jwk) || !length(jwk$kty)) 17 | stop("File does not have jwk data") 18 | key <- switch(tolower(jwk$kty), 19 | "ec" = jwk_parse_ec(jwk), 20 | "rsa" = jwk_parse_rsa(jwk), 21 | "oct" = return(jwk_parse_oct(jwk)), #oct is just bytes 22 | stop("Unknown key type: ", jwk$kty) 23 | ) 24 | pubkey <- if(inherits(key, "key")){ 25 | openssl:::derive_pubkey(key) 26 | } else { 27 | key 28 | } 29 | type <- openssl:::pubkey_type(pubkey) 30 | structure(key, class = c(class(key), type)) 31 | } 32 | 33 | # Former name 34 | #' @export 35 | jwk_read <- read_jwk 36 | 37 | jwk_parse_ec <- function(input){ 38 | curve <- toupper(input$crv) 39 | x <- bignum(base64url_decode(input$x)) 40 | y <- bignum(base64url_decode(input$y)) 41 | if(length(input$d)){ 42 | d <- bignum(base64url_decode(input$d)) 43 | key <- openssl:::ecdsa_key_build(x, y, d, curve) 44 | structure(key, class = "key") 45 | } else { 46 | pubkey <- openssl:::ecdsa_pubkey_build(x, y, curve) 47 | structure(pubkey, class = "pubkey") 48 | } 49 | } 50 | 51 | #' @importFrom openssl bignum 52 | jwk_parse_rsa <- function(input){ 53 | e <- bignum(base64url_decode(input$e)) 54 | n <- bignum(base64url_decode(input$n)) 55 | if(length(input$d)){ 56 | p <- bignum(base64url_decode(input$p)) 57 | q <- bignum(base64url_decode(input$q)) 58 | d <- bignum(base64url_decode(input$d)) 59 | key <- openssl:::rsa_key_build(e, n, p, q, d) 60 | structure(key, class = "key") 61 | } else { 62 | pubkey <- openssl:::rsa_pubkey_build(e, n) 63 | structure(pubkey, class = "pubkey") 64 | } 65 | } 66 | 67 | jwk_parse_oct <- function(input){ 68 | base64url_decode(input$k) 69 | } 70 | -------------------------------------------------------------------------------- /R/write.R: -------------------------------------------------------------------------------- 1 | #' JSON web-keys 2 | #' 3 | #' Read and write RSA, ECDSA or AES keys as JSON web keys. 4 | #' 5 | #' @export 6 | #' @rdname jwk 7 | #' @name jwk 8 | #' @aliases jwk_write 9 | #' @param x an RSA or EC key or pubkey file 10 | #' @param path file path to write output 11 | #' @examples # generate an ecdsa key 12 | #' library(openssl) 13 | #' key <- ec_keygen("P-521") 14 | #' write_jwk(key) 15 | #' write_jwk(as.list(key)$pubkey) 16 | #' 17 | #' # Same for RSA 18 | #' key <- rsa_keygen() 19 | #' write_jwk(key) 20 | #' write_jwk(as.list(key)$pubkey) 21 | write_jwk <- function(x, path = NULL){ 22 | str <- jwk_export(x) 23 | if(is.null(path)) return(str) 24 | writeLines(str, path) 25 | invisible(path) 26 | } 27 | 28 | # Old name 29 | #' @export 30 | jwk_write <- write_jwk 31 | 32 | jwk_export <- function(x, ...){ 33 | UseMethod("jwk_export") 34 | } 35 | 36 | jwk_export.dsa <- function(x, ...){ 37 | stop("JWK does not support DSA keys. Try RSA or ECDSA instead") 38 | } 39 | 40 | jwk_export.ecdsa <- function(x, ...){ 41 | keydata <- as.list(x)$data 42 | out <- list ( 43 | kty = "EC", 44 | crv = keydata$curve, 45 | x = base64url_encode(keydata$x), 46 | y = base64url_encode(keydata$y) 47 | ) 48 | if(length(keydata$secret)) 49 | out$d <- base64url_encode(keydata$secret) 50 | to_json(out) 51 | } 52 | 53 | jwk_export.rsa <- function(x, ...){ 54 | keydata <- as.list(x)$data 55 | out <- lapply(keydata, base64url_encode) 56 | out$kty <- "RSA" 57 | to_json(out) 58 | } 59 | 60 | jwk_export.raw <- function(x, ...){ 61 | if(is.na(match(length(x), c(16, 24, 32, 48, 64)))) 62 | stop("Raw key must length 16, 24, 32 (AES) or 32, 48, 64 (HMAC)") 63 | to_json(list( 64 | kty = "oct", 65 | k = base64url_encode(x) 66 | )) 67 | } 68 | -------------------------------------------------------------------------------- /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 | # To compile dev version of openssl 14 | environment: 15 | global: 16 | USE_RTOOLS: true 17 | 18 | build_script: 19 | - travis-tool.sh install_deps 20 | 21 | test_script: 22 | - travis-tool.sh run_tests 23 | 24 | on_failure: 25 | - 7z a failure.zip *.Rcheck\* 26 | - appveyor PushArtifact failure.zip 27 | 28 | artifacts: 29 | - path: '*.Rcheck\**\*.log' 30 | name: Logs 31 | 32 | - path: '*.Rcheck\**\*.out' 33 | name: Logs 34 | 35 | - path: '*.Rcheck\**\*.fail' 36 | name: Logs 37 | 38 | - path: '*.Rcheck\**\*.Rout' 39 | name: Logs 40 | 41 | - path: '\*_*.tar.gz' 42 | name: Bits 43 | 44 | - path: '\*_*.zip' 45 | name: Bits 46 | -------------------------------------------------------------------------------- /inst/WORDLIST: -------------------------------------------------------------------------------- 1 | AES 2 | AppVeyor 3 | ECDSA 4 | Github 5 | HMAC 6 | IETF 7 | JSON 8 | JWA 9 | JWE 10 | JWK 11 | JWS 12 | JWT 13 | LetsEncrypt 14 | NumericDate 15 | OAuth 16 | RSA 17 | RStudio 18 | StringOrURI 19 | WebCryptoAPI 20 | bitsize 21 | https 22 | io 23 | json 24 | jwt 25 | natively 26 | nd 27 | openssl 28 | pubkey 29 | rfc 30 | sha 31 | workgroup 32 | -------------------------------------------------------------------------------- /jose.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,namespace 22 | -------------------------------------------------------------------------------- /man/base64url_encode.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/base64url.R 3 | \name{base64url_encode} 4 | \alias{base64url_encode} 5 | \alias{base64url_decode} 6 | \title{Base64URL encoding} 7 | \usage{ 8 | base64url_encode(bin) 9 | 10 | base64url_decode(text) 11 | } 12 | \arguments{ 13 | \item{bin}{a binary blob to encode} 14 | 15 | \item{text}{a base64url encoded string} 16 | } 17 | \description{ 18 | The \code{base64url_encode} functions are a variant of the standard base64. They are 19 | specified in Section 5 of RFC 4648 as a URL-safe alternative. They use different symbols 20 | for the 62:nd and 63:rd alphabet character and do not include trailing \code{==} 21 | padding. 22 | } 23 | -------------------------------------------------------------------------------- /man/jwk.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/read.R, R/write.R 3 | \name{read_jwk} 4 | \alias{read_jwk} 5 | \alias{jwk_read} 6 | \alias{jwk} 7 | \alias{write_jwk} 8 | \alias{jwk_write} 9 | \title{JSON web-keys} 10 | \usage{ 11 | read_jwk(file) 12 | 13 | write_jwk(x, path = NULL) 14 | } 15 | \arguments{ 16 | \item{file}{path to file with key data or literal json string} 17 | 18 | \item{x}{an RSA or EC key or pubkey file} 19 | 20 | \item{path}{file path to write output} 21 | } 22 | \description{ 23 | Read and write RSA, ECDSA or AES keys as JSON web keys. 24 | } 25 | \examples{ 26 | # generate an ecdsa key 27 | library(openssl) 28 | key <- ec_keygen("P-521") 29 | write_jwk(key) 30 | write_jwk(as.list(key)$pubkey) 31 | 32 | # Same for RSA 33 | key <- rsa_keygen() 34 | write_jwk(key) 35 | write_jwk(as.list(key)$pubkey) 36 | } 37 | -------------------------------------------------------------------------------- /man/jwt_claim.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/claim.R 3 | \name{jwt_claim} 4 | \alias{jwt_claim} 5 | \title{Generate claim} 6 | \usage{ 7 | jwt_claim( 8 | iss = NULL, 9 | sub = NULL, 10 | aud = NULL, 11 | exp = NULL, 12 | nbf = NULL, 13 | iat = Sys.time(), 14 | jti = NULL, 15 | ... 16 | ) 17 | } 18 | \arguments{ 19 | \item{iss}{(Issuer) Claim, should be rfc7519 'StringOrURI' value} 20 | 21 | \item{sub}{(Subject) Claim, should be rfc7519 'StringOrURI' value} 22 | 23 | \item{aud}{(Audience) Claim, should contain one or rfc7519 'StringOrURI' values} 24 | 25 | \item{exp}{(Expiration Time) Claim, should be rfc7519 'NumericDate' value; R 26 | \code{POSIXct} values are automatically coerced.} 27 | 28 | \item{nbf}{(Not Before) Claim, should be rfc7519 'NumericDate' value; R 29 | \code{POSIXct} values are automatically coerced.} 30 | 31 | \item{iat}{(Issued At) Claim, should be rfc7519 'NumericDate' value; R 32 | \code{POSIXct} values are automatically coerced.} 33 | 34 | \item{jti}{(JWT ID) Claim, optional unique identifier for the JWT} 35 | 36 | \item{...}{additional custom claims to include} 37 | } 38 | \description{ 39 | Helper function to create a named list used as the claim of a JWT payload. 40 | See \url{https://datatracker.ietf.org/doc/html/rfc7519#section-4.1} for details. 41 | } 42 | -------------------------------------------------------------------------------- /man/jwt_encode.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/jwt.R 3 | \name{jwt_encode_hmac} 4 | \alias{jwt_encode_hmac} 5 | \alias{jwt} 6 | \alias{jose} 7 | \alias{jwt_decode_hmac} 8 | \alias{jwt_encode_sig} 9 | \alias{jwt_decode_sig} 10 | \alias{jwt_split} 11 | \title{JSON Web Token} 12 | \usage{ 13 | jwt_encode_hmac(claim = jwt_claim(), secret, size = 256, header = NULL) 14 | 15 | jwt_decode_hmac(jwt, secret) 16 | 17 | jwt_encode_sig(claim = jwt_claim(), key, size = 256, header = NULL) 18 | 19 | jwt_decode_sig(jwt, pubkey) 20 | 21 | jwt_split(jwt) 22 | } 23 | \arguments{ 24 | \item{claim}{a named list with fields to include in the jwt payload} 25 | 26 | \item{secret}{string or raw vector with a secret passphrase} 27 | 28 | \item{size}{bitsize of sha2 signature, i.e. \code{sha256}, \code{sha384} or \code{sha512}. 29 | Only for HMAC/RSA, not applicable for ECDSA keys.} 30 | 31 | \item{header}{named list with additional parameter fields to include in the jwt header as 32 | defined in \href{https://datatracker.ietf.org/doc/html/rfc7515#section-9.1.2}{rfc7515 section 9.1.2}} 33 | 34 | \item{jwt}{string containing the JSON Web Token (JWT)} 35 | 36 | \item{key}{path or object with RSA or EC private key, see \link[openssl:read_key]{openssl::read_key}.} 37 | 38 | \item{pubkey}{path or object with RSA or EC public key, see \link[openssl:read_pubkey]{openssl::read_pubkey}.} 39 | } 40 | \description{ 41 | Sign or verify a JSON web token. The \code{jwt_encode_hmac}, \code{jwt_encode_rsa}, 42 | and \code{jwt_encode_ec} default to \code{HS256}, \code{RS256}, and \code{ES256} 43 | respectively. See \href{https://jwt.io}{jwt.io} or 44 | \href{https://datatracker.ietf.org/doc/html/rfc7519}{RFC7519} for more details. 45 | } 46 | \examples{ 47 | # HMAC signing 48 | mysecret <- "This is super secret" 49 | token <- jwt_claim(name = "jeroen", session = 123456) 50 | sig <- jwt_encode_hmac(token, mysecret) 51 | jwt_decode_hmac(sig, mysecret) 52 | 53 | # RSA encoding 54 | mykey <- openssl::rsa_keygen() 55 | pubkey <- as.list(mykey)$pubkey 56 | sig <- jwt_encode_sig(token, mykey) 57 | jwt_decode_sig(sig, pubkey) 58 | 59 | # Same with EC 60 | mykey <- openssl::ec_keygen() 61 | pubkey <- as.list(mykey)$pubkey 62 | sig <- jwt_encode_sig(token, mykey) 63 | jwt_decode_sig(sig, pubkey) 64 | 65 | # Get elements of the key 66 | mysecret <- "This is super secret" 67 | token <- jwt_claim(name = "jeroen", session = 123456) 68 | jwt <- jwt_encode_hmac(token, mysecret) 69 | jwt_split(jwt) 70 | } 71 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # jose 2 | 3 | [![Build Status](https://app.travis-ci.com/jeroen/jose.svg?branch=master)](https://app.travis-ci.com/jeroen/jose) 4 | [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/jeroen/jose?branch=master&svg=true)](https://ci.appveyor.com/project/jeroen/jose) 5 | [![Coverage Status](https://codecov.io/github/jeroen/jose/coverage.svg?branch=master)](https://app.codecov.io/github/jeroen/jose?branch=master) 6 | [![CRAN_Status_Badge](http://www.r-pkg.org/badges/version/jose)](http://cran.r-project.org/package=jose) 7 | [![CRAN RStudio mirror downloads](http://cranlogs.r-pkg.org/badges/jose)](http://cran.r-project.org/web/packages/jose/index.html) 8 | 9 | > JavaScript Object Signing and Encryption 10 | 11 | Read and write JSON Web Keys (JWK, rfc7517), generate and verify JSON 12 | Web Signatures (JWS, rfc7515) and encode/decode JSON Web Tokens (JWT, rfc7519). 13 | These standards provide modern signing and encryption formats that are natively 14 | supported by browsers via the JavaScript WebCryptoAPI, and used by services 15 | like OAuth 2.0, LetsEncrypt, and Github Apps. 16 | 17 | ## Documentation 18 | 19 | Vignettes for the R package: 20 | 21 | - [Reading/Writing JSON Web Keys (JWK) in R](https://cran.r-project.org/web/packages/jose/vignettes/jwk.html) 22 | - [Encoding/Decoding JSON Web Tokens (JWT) in R](https://cran.r-project.org/web/packages/jose/vignettes/jwt.html) 23 | 24 | Specifications and standards: 25 | 26 | - JOSE RFC Tracker: https://datatracker.ietf.org/wg/jose/documents/ 27 | - Browser WebCryptoAPI API: https://www.w3.org/TR/WebCryptoAPI/#jose 28 | - ACME Protocol (LetsEncrypt): https://ietf-wg-acme.github.io/acme/draft-ietf-acme-acme.html 29 | 30 | ## JSON Web Keys (JWK) 31 | 32 | ```r 33 | library(jose) 34 | 35 | # generate an ecdsa key 36 | key <- ec_keygen("P-521") 37 | write_jwk(key) 38 | write_jwk(as.list(key)$pubkey) 39 | 40 | # Same for RSA 41 | key <- rsa_keygen() 42 | write_jwk(key) 43 | write_jwk(as.list(key)$pubkey) 44 | ``` 45 | 46 | ## JSON Web Tokens (JWT) 47 | 48 | ```r 49 | # HMAC signing 50 | mysecret <- "This is super secret" 51 | token <- jwt_claim(name = "jeroen", session = 123456) 52 | sig <- jwt_encode_hmac(token, mysecret) 53 | jwt_decode_hmac(sig, mysecret) 54 | 55 | # RSA encoding 56 | mykey <- openssl::rsa_keygen() 57 | pubkey <- as.list(mykey)$pubkey 58 | sig <- jwt_encode_sig(token, mykey) 59 | jwt_decode_sig(sig, pubkey) 60 | 61 | # Same with EC 62 | mykey <- openssl::ec_keygen() 63 | pubkey <- as.list(mykey)$pubkey 64 | sig <- jwt_encode_sig(token, mykey) 65 | jwt_decode_sig(sig, pubkey) 66 | ``` 67 | -------------------------------------------------------------------------------- /tests/js/aes.js: -------------------------------------------------------------------------------- 1 | function str2buf(str) { 2 | var bufView = new Uint8Array(str.length); 3 | for (var i=0, strLen=str.length; i 7 | %\VignetteIndexEntry{Reading/Writing JSON Web Keys (JWK) in R} 8 | %\VignetteEngine{knitr::rmarkdown} 9 | \usepackage[utf8]{inputenc} 10 | --- 11 | 12 | ```{r setup, include=FALSE} 13 | knitr::opts_chunk$set(echo = TRUE) 14 | knitr::opts_chunk$set(comment = "") 15 | ``` 16 | 17 | 18 | ### RSA / ECDSA keys 19 | 20 | JSON Web Keys (JWK) is a format specified in [RFC7517](https://datatracker.ietf.org/doc/html/rfc7517) for storing RSA/EC/AES keys in a JSON based format. It can be used to import/export such keys in the browser using the new [W3C WebCryptoAPI](https://www.w3.org/TR/WebCryptoAPI/). 21 | 22 | The `jose` package makes it easy to read/write such keys in R for use with JWT or any other functionality from the `openssl` package. 23 | 24 | 25 | ```{r} 26 | library(openssl) 27 | library(jose) 28 | 29 | # Generate a ECDSA key 30 | key <- openssl::ec_keygen() 31 | jsonlite::prettify(write_jwk(key)) 32 | 33 | # Use public key 34 | pubkey <- as.list(key)$pubkey 35 | json <- write_jwk(pubkey) 36 | jsonlite::prettify(json) 37 | 38 | # Read JWK key 39 | (out <- read_jwk(json)) 40 | identical(pubkey, out) 41 | ``` 42 | 43 | 44 | ### AES/HMAC keys 45 | 46 | JWT also specifies a format for encoding AES/HMAC secrets. Such secret keys are simply raw bytes. 47 | 48 | ```{r} 49 | # Random secret 50 | (key <- rand_bytes(16)) 51 | (jwk <- write_jwk(key)) 52 | read_jwk(jwk) 53 | ``` 54 | -------------------------------------------------------------------------------- /vignettes/jwt.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Encoding/Decoding JSON Web Tokens (JWT) in R" 3 | date: "`r Sys.Date()`" 4 | output: 5 | html_document 6 | vignette: > 7 | %\VignetteIndexEntry{Encoding/Decoding JSON Web Tokens (JWT) in R} 8 | %\VignetteEngine{knitr::rmarkdown} 9 | \usepackage[utf8]{inputenc} 10 | --- 11 | 12 | ```{r setup, include=FALSE} 13 | knitr::opts_chunk$set(echo = TRUE) 14 | knitr::opts_chunk$set(comment = "") 15 | ``` 16 | 17 | JavaScript Object Signing and Encryption (JOSE) consists of a set of specifications for encryption and signatures based on the popular JSON format. This is work in progress, the IETF [jose workgroup](https://datatracker.ietf.org/wg/jose/) usually has the latest information. 18 | 19 | - [RFC7515](https://datatracker.ietf.org/doc/html/rfc7515): JSON Web Signature (JWS) 20 | - [RFC7516](https://datatracker.ietf.org/doc/html/rfc7516): JSON Web Encryption (JWE) 21 | - [RFC7517](https://datatracker.ietf.org/doc/html/rfc7517): JSON Web Key (JWK) 22 | - [RFC7518](https://datatracker.ietf.org/doc/html/rfc7518): JSON Web Algorithms (JWA) 23 | - [RFC7519](https://datatracker.ietf.org/doc/html/rfc7519): JSON Web Token (JWT) 24 | 25 | The `jose` package implements some of these specifications, in particular for working with JSON web tokens and keys. 26 | 27 | ### JSON Web Token: HMAC tagging 28 | 29 | The most common use of JSON Web Tokens is combining a small payload (the 'claim') with a HMAC tag or RSA/ECDSA signature. See also [https://jwt.io](https://jwt.io) for short introduction. 30 | 31 | ```{r} 32 | library(openssl) 33 | library(jose) 34 | 35 | # Example payload 36 | claim <- jwt_claim(user = "jeroen", session_key = 123456) 37 | 38 | # Encode with hmac 39 | key <- charToRaw("SuperSecret") 40 | (jwt <- jwt_encode_hmac(claim, secret = key)) 41 | 42 | # Decode 43 | jwt_decode_hmac(jwt, secret = key) 44 | ``` 45 | 46 | The decoding errors if the tag verification fails. 47 | 48 | ```{r error=TRUE} 49 | # What happens if we decode with the wrong key 50 | jwt_decode_hmac(jwt, secret = raw()) 51 | ``` 52 | 53 | ### JSON Web Token: RSA/ECDSA signature 54 | 55 | Similarly, we can use an RSA or ECDSA key pair we to verify a signature from someone's public key. 56 | 57 | ```{r} 58 | # Generate ECDSA keypair 59 | key <- ec_keygen() 60 | pubkey <- as.list(key)$pubkey 61 | 62 | # Sign with the private key 63 | (jwt <- jwt_encode_sig(claim, key = key)) 64 | 65 | # Decode and verify using the public key 66 | jwt_decode_sig(jwt, pubkey = pubkey) 67 | ``` 68 | 69 | Again decoding will error if the signature verification fails. 70 | 71 | ```{r error = TRUE} 72 | wrong_key <- ec_keygen() 73 | jwt_decode_sig(jwt, pubkey = wrong_key) 74 | ``` 75 | 76 | 77 | The spec also describes methods for encrypting the payload, but this is currently not widely in use yet. 78 | 79 | ### Reserved jwt-claim names 80 | 81 | You can include custom fields in your jwt payload, but the spec names a few [registered claims](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1) that are reserved for specific uses. 82 | 83 | - `iss` (Issuer): the principal that issued the JWT. 84 | - `sub` (Subject): the principal that is the subject of the JWT. 85 | - `aud` (Audience): the recipients that the JWT is intended for. 86 | - `exp` (Expiration Time): the expiration time on or after which the JWT must not be accepted. 87 | - `nbf` (Not Before): the time before which the JWT must not be accepted. 88 | - `iat` (Issued At): the time at which the JWT was issued. 89 | - `jti` (JWT ID): a unique identifier for the JWT. 90 | 91 | Each of these are optional, by default only `iat` is set. The `jwt_claim()` function will automatically do basic validation when you set additional fields from this list. For any other fields you can use any value. For example: 92 | 93 | ```{r} 94 | # Note that this token expires in 1 hour! 95 | myclaim <- jwt_claim( 96 | iss = "My webapp", 97 | exp = Sys.time() + 3600, 98 | myfield = "Some application logic", 99 | customer = "a cow" 100 | ) 101 | (jwt <- jwt_encode_sig(myclaim, key = key)) 102 | ``` 103 | 104 | The decode functions will automatically verify that the token has not expired (with a 60s grace period to account for inaccurate clocks), and error otherwise: 105 | 106 | ```{r} 107 | jwt_decode_sig(jwt, pubkey = pubkey) 108 | ``` 109 | 110 | 111 | ### Where is the JSON 112 | 113 | The jwt payloads consists of a head, body and signature which are separated with a dot into a single string. Both the header and body are actually `base64url` encoded JSON objects. 114 | 115 | ```{r} 116 | (strings <- strsplit(jwt, ".", fixed = TRUE)[[1]]) 117 | cat(rawToChar(base64url_decode(strings[1]))) 118 | cat(rawToChar(base64url_decode(strings[2]))) 119 | ``` 120 | 121 | However you should never trust this information without verifying the signature. This is what the `jwt_decode` functions do for you. 122 | --------------------------------------------------------------------------------