├── .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 | [](https://app.travis-ci.com/jeroen/jose)
4 | [](https://ci.appveyor.com/project/jeroen/jose)
5 | [](https://app.codecov.io/github/jeroen/jose?branch=master)
6 | [](http://cran.r-project.org/package=jose)
7 | [](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 |
--------------------------------------------------------------------------------