Search code examples
rfernet

How do I decode fernet encryption in R?


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.


Solution

  • 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.