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