TOTP with Perl and Authen::OATH
I wrote this post after seeing Flavio Poletti's blog post entitled OATH Toolkit. I have been a fan of time-based one time passwords (TOTP) for many years. In fact, I used Mobile-OTP in commercial applications for several years before the Initiative for Open Authentication (OATH) and OATH/TOTP were codified in RFC6238.
Now-a-days, OATH/TOTP is the best choice for time-based one time passwords, and has been for at least a decade.
Introduction
I have implemented OATH/TOTP many times in my career, and in several programming languages, including Perl. One of those Perl implementations is within the open source program kpcli.
Using Authen::OATH for TOTP
Using Authen::OATH for TOTP is straightforward. The kpcli code appears a little overly complex because it optionally demand-loads the Authen::OATH module and has support for digest ciphers other than SHA1. This is kpcli's get_totp() subroutine:
sub get_totp($$) {
my $key2FA = shift @_ || '';
my $digest = shift @_ || 'SHA'; # RFC6238 uses SHA-1
my $oath = Authen::OATH->new( digest => 'Digest::'.uc($digest) );
my $otp = $oath->totp(decode_base32($key2FA));
return $otp;
}
Breaking that down, what we see is that a new Authen::OATH object is created with Digest::SHA by default (although others are supported). The totp() method is then used to obtain the OTP from the base32 decoded shared key (base32 storage is codified in the OATH TOTP standard). Note that decode_base32() is from Convert::Base32. The resulting OTP is a 6-digit, OATH-compliant, TOTP.
Here is a working snippet with limited complexity:
#!/usr/bin/perl
use strict;
use warnings;
use Authen::OATH;
use Convert::Base32 qw(decode_base32);
my $key = '42R6MQ7PQNVY5NMR7VAJUBTK';
my $oath = Authen::OATH->new( digest => 'Digest::SHA' );
my $otp = $oath->totp(decode_base32($key));
print "$otp\n";
In a client-side implementation, that simple example is about all that you'll need.
In a server-side implementation, you'll want to accept a few OTPs from ± the current time. A second parameter can be given to Authen::OATH->totp() to manually set the time used to generate the TOTP, and so a simplified server-side implementation might look like this:
use strict;
use warnings;
use Authen::OATH;
use Convert::Base32 qw(decode_base32);
my $key = '42R6MQ7PQNVY5NMR7VAJUBTK';
my $test_otp = '123456';
my $timestep = 30; # 30 second time steps
my $oath = Authen::OATH->new( digest => 'Digest::SHA' );
my $otp = $oath->totp(decode_base32($key));
if (is_valid_totp($key,$test_otp,3,2)) {
print "Valid TOTP\n";
} else {
print "Invalid TOTP\n";
}
exit;
sub is_valid_totp {
my $key = shift @_;
my $test_otp = shift @_;
my $num_prior = shift @_ || 0;
my $num_after = shift @_ || 0;
$num_prior = abs($num_prior) * -1;
$num_after = abs($num_prior);
my $oath = Authen::OATH->new( digest => 'Digest::SHA' );
my $now = time;
foreach my $period ($num_prior..$num_after) {
my $test_time = $now + ($period * $timestep);
my $otp = $oath->totp($key, $test_time);
#warn "Debug: period=$period now=$now test_time=$test_time\n";
#warn "Debug: period=$period otp=$otp test_otp=$test_otp\n";
if ($otp eq $test_otp) { return 1; } # Match
}
return 0;
}
Closing
I strongly encourage everyone to use 2FA/TOTP on all of the systems that support it, and particularly ones of high importance to you. On mobile, I use the Google Authenticator app and, on my Linux desktop, a mix of my kpcli and otp programs.
I hope that this post is helpful to someone.
Leave a comment