1

I have a string that was encrypted in python using the Fernet algorithm (https://cryptography.io/en/latest/fernet/). How can I read this value in R?

Here's the python code which gives some example data

import base64
from cryptography.fernet import Fernet

key = base64.urlsafe_b64encode(b"ThisIsTheRealKeyItIsAVeryGoodOne")
fernet = Fernet(key)
fernet.encrypt(b"hello")

So a sample input string is

gAAAAABkCluAsIXI905vOlZsut1CvVtboIZ2_NHalTQLbsZv_ogl0reBBWg4v0UlTcg5aqMRzBfFxKBGVGUHYqepeNQb8wpfgQ==

And I want to get "hello" back out given the key "ThisIsTheRealKeyItIsAVeryGoodOne" and the encrypted string above. How can I decode this value in R?

I do know that each time I run the python code a different output is produced, but I assume they can all be coded in the same way.

MrFlick
  • 195,160
  • 17
  • 277
  • 295

1 Answers1

1

I couldn't find an R package that would do the encryption automatically for you in R, but It's possible to put together code using many of the encryption features of openssl. Let's define a few helper functions

library(openssl)

url_unsafe <- function(x) gsub("_", "/" ,gsub("-", "+", x, fixed=TRUE), fixed=TRUE)
url_safe <- function(x) gsub("/", "_" ,gsub("+", "-", x, fixed=TRUE), fixed=TRUE)

split_raw_message <- function(token) {
  if (is.character(token)) {
    bytes <- openssl::base64_decode(url_unsafe(token))
  } else if (is.raw(token)) {
    bytes <- token
  } else {
    stop("token should be base64 encoded string or raw vector")
  }
  stopifnot("message too short"=length(bytes)>1+8+16+23)
  stopifnot("missing magic number"=bytes[1]==0x80)
  list(
    magic = bytes[1],
    ts = bytes[2:9],
    iv = bytes[10:25],
    payload = bytes[26:(length(bytes)-32)],
    signature = utils::tail(bytes, 32)
  )
}

validate_signature <- function(message, key) {
  all(as.raw(openssl::sha256(with(message, c(magic, ts, iv,  payload)), key=key)) == message$signature)
}

sign_message <- function(message, key) {
  message$signature <- as.raw(openssl::sha256(with(message, as.raw(c(magic, ts, iv, payload))), key=key))
  message
}

decode_message_raw <- function(encrypted_string, keys) {
  message <- split_raw_message(encrypted_string)
  stopifnot(validate_signature(message, keys$sign))
  list(
    content = rawToChar(openssl::aes_cbc_decrypt(message$payload, keys$encrypt, iv = message$iv)),
    ts = as.POSIXct(readBin(message$ts, "integer", n=1, size=8, endian = "big"), origin="1970-01-01")
  )
}

decode_message <- function(encrypted_string, keys) {
  decode_message_raw(encrypted_string, keys)$content
}

build_message <- function(content, keys, iv=openssl::rand_bytes(16), ts=Sys.time()) {
  if (inherits(ts, "POSIXt")) {
    ts <- writeBin(as.numeric(ts), raw(0), size=8, endian="big")
  }
  message <- list(
    magic = as.raw(0x80),
    ts = ts,
    iv = as.raw(iv),
    payload = c(openssl::aes_cbc_encrypt(charToRaw(content), keys$encrypt, iv = iv))
  )
  message <- sign_message(message, keys$sign)
  message
}

encode_message <- function(content, keys) {
  message <- build_message(content, keys)
  url_safe(openssl::base64_encode(with(message, c(magic, ts, iv, payload, signature))))
}

The main function you need to decode the message is decode_message. Note that the python fernet key is actually made up of two parts. So in R we will split the 32 bit key into the encrytion part and the signing part

fernet_keys <- list(
  sign = charToRaw("ThisIsTheRealKey"),
  encrypt = charToRaw("ItIsAVeryGoodOne")
)

Now we can decode the message.

encoded <- "gAAAAABkCluAsIXI905vOlZsut1CvVtboIZ2_NHalTQLbsZv_ogl0reBBWg4v0UlTcg5aqMRzBfFxKBGVGUHYqepeNQb8wpfgQ=="
decode_message(encoded, fernet_keys)
# [1] "hello"

We can also encode values in R, but note that it's unlikely you will get the exact same output because the encoded value uses a random initialization vector each time and a timestamp. Note this decoding function doesn't check the timestamp, but if you use decode_message_raw rather than decode_message you can get the timestamp and use that to decide if you want to trust the message.

MrFlick
  • 195,160
  • 17
  • 277
  • 295