I would like to add two-factor authentication to a shiny application, so I've been trying to work out the google authenticator implementation of TOTP in R. I can create a six-digit number, but mine doesn't match the app's. My best guess is that the SHA1 is not being executed properly, where I hash using the time-key and then use that hash as the key for a second hash. Or maybe I'm not getting the lower nibble or sign-bit mask right?
Can anyone see where I'm going wrong?
I'm using this site as a guide, including the link to his Go code: https://garbagecollected.org/2014/09/14/how-google-authenticator-works/
I've also tried digest::digest(x, algo="sha1", serialize=FALSE)
, but it doesn't let me specify a key so I don't know if that can work.
library(qrcode)
library(openssl)
library(base64url)
secret <- "supersecret"
secret32 <- base64url::base32_encode(secret)
secretmsg <- paste0("otpauth://totp/R%20Authenticator%20test?secret=", secret32, "&issuer=test")
qrcode::qrcode_gen(secretmsg)
# use math to convert from raw to integer, allow for lower nibble and unsign
rawToInt <- function(x, part=NULL, bits=FALSE) {
if(!bits) x <- rawToBits(x)
logivect <- as.logical(x)
if(length(part)) {
if(part == "l") logivect <- logivect[1:4] # lower nibble
else if(part == "m") logivect[length(logivect)] <- FALSE # mask most sig. bit
}
sum(2^(which(logivect)-1))
}
# Generate a six-digit number
authenticator <- function(secret) {
# Unix time
n <- as.numeric(Sys.time())
# The hash
hmac <- openssl::sha1(secret, key=openssl::sha1(secret, key=format(n%/%30)))
# Convert hash to raw
hmac_raw <- charToRaw(hmac)
# Take the last byte
last_byte <- hmac_raw[(length(hmac_raw)-1):length(hmac_raw)]
# Use only the first 4 bits (lower nibble), R puts least sig bits at left
offset <- rawToInt(last_byte, part='l')
# multiply by 2 because R reports each byte as 2 hex chars,
# add 1 because R indexes from 1
offset <- (offset*2+1):(offset*2+8)
four_bytes <- hmac_raw[offset]
# Convert to int
large_integer <- readBin(four_bytes, integer(), size=4)
# Mask most sig. bit
large_integer <- intToBits(large_integer)
large_integer[length(large_integer)] <- as.raw(0)
large_integer <- rawToInt(large_integer, bits=TRUE)
small_integer <- large_integer %% 1e6
# Make it print pretty
nc <- nchar(small_integer)
small_int_print <- paste0(rep(0, 6-nc), format(small_integer, big.mark=""))
# Time remaining
remain <- round(30-n%%30)
cat(
remain, "seconds remaining\n",
small_int_print, "\n"
)
invisible(small_integer)
}
authenticator(secret=secret)