#! /usr/bin/perl -w # # generate TOTP tokens based on RFC 6238 using base-32 encoded # secrets, as stored by e.g. andOTP for Android. Use the plain-text # backup function of andOTP to generate the JSON files used by this # utility. For andOTP, see https://github.com/andOTP/andOTP and # https://play.google.com/store/apps/details?id=org.shadowice.flocke.andotp # # JSON format used here (and by andOTP backup) is an array of objects. # only the "label" and "issuer" fields, and obviously the "secret" elements # are used: # # [ # { # "algorithm" : "SHA1", # "digits" : 6, # "issuer" : "Oracle", # "label" : "username @ tenantdomain.example", # "last_used" : 1629225821795, # "period" : 30, # "secret" : "XXXXXXXXXXXXXXXX", # "tags" : [], # "thumbnail" : "Default", # "type" : "TOTP", # "used_frequency" : 666 # }, # { # "algorithm" : "SHA1", # ... # } # ] # # # ----- standard obligatory license blurb ---------------------------------- # # Copyright 2022 David Groep, Nikhef (Stichting NWO-I) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # -------------------------------------------------------------------------- # use Getopt::Long; use JSON; use MIME::Base32; use Digest::HMAC_SHA1 qw/ hmac_sha1_hex /; my $jsonpat=$ENV{"ANDOTPJSON"}?$ENV{"ANDOTPJSON"}:"/m/doc/otp_accounts_*.json"; my $jsonfile; my $jsonfiledate=0; my $jsondata; my $help; &GetOptions( 's|jsonsource=s' => \$jsonpat, 'h|help' => \$help ) or exit 1; if ( $help or $#ARGV < 0 ) { print < $jsonfiledate ) { $jsonfiledate = (stat(_))[9]; $jsonfile = $f; } } die "Cannot find totp secrets file in $jsonpat.\n" unless $jsonfile; print "Generating TOTP tokens based on $jsonfile\n"; { local(*INPUT, $/); open (INPUT, $jsonfile) || die "can't open $jsonfile: $!"; $jsondata = ; } my $jsonref = decode_json $jsondata; foreach my $entry ( @$jsonref ) { next unless $entry->{"type"} eq "TOTP"; next unless defined $entry->{"secret"}; if ( (defined ($entry->{"issuer"}) and $entry->{"issuer"} =~ /$ARGV[0]/i) or (defined ($entry->{"label"}) and $entry->{"label"} =~ /$ARGV[0]/i) ) { printf "%06d -- %s (%s)\n", totp_token($entry->{"secret"}), $entry->{"label"}?$entry->{"label"}:"-", $entry->{"issuer"}?$entry->{"issuer"}:"-"; } } # ########################################################################## sub totp_token { my $secret = shift; my $key = unpack("H*", decode_base32($secret)); my $lpad_time = sprintf("%016x", int(time()/30)); my $hmac = hmac_sha1_hex_string($lpad_time, $key); my $offset = sprintf("%d", hex(substr($hmac, -1))); my $part1 = 0 + sprintf("%d", hex(substr($hmac, $offset*2, 8))); my $part2 = 0 + sprintf("%d", hex("7fffffff")); my $token = substr("".($part1 & $part2), -6); return $token; } sub hmac_sha1_hex_string { my ($data, $key) = map pack('H*', $_), @_; hmac_sha1_hex($data, $key); }