Created
February 6, 2018 11:53
-
-
Save tron1point0/f989a6fa2baf19313bf367affa7f05ab to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env perl | |
use v5.16; | |
use warnings; | |
use Digest::HMAC_SHA1 qw(hmac_sha1); | |
use Convert::Base32 qw(decode_base32); | |
use Getopt::Long; | |
use Pod::Usage; | |
my %GLOBAL = ( | |
time => time, | |
verbose => 0, | |
help => 0, | |
man => 0, | |
config => [], | |
query => '', | |
names => 1, | |
add => '', | |
); | |
sub LoadFile { | |
my ($file) = (@_,$_); | |
return unless -r $file; | |
my @tdefs = (); | |
my $def = {}; | |
open my $fh, '<', $file; | |
while (<$fh>) { | |
chomp; | |
s/#.*$//; # Strip comments | |
s/^\s*//; # trim leading | |
s/\s*$//; # trim trailing | |
if (m/^---/ && $. > 1) { | |
push @tdefs, $def; | |
$def = {}; | |
next; | |
} | |
if (m/^(.*?)\s*:\s*(.*)$/) { | |
$def->{$1} = $2; | |
next; | |
} | |
} | |
push @tdefs, $def; | |
@tdefs; | |
} | |
sub debug { | |
chomp $_[-1]; | |
warn @_, "\n"; | |
} | |
sub verbose(&;@) { | |
my ($fn, $level, @ret) = @_; | |
$level //= 1; | |
@ret = ($_) unless @ret; | |
{ local ($_) = @ret; | |
local ($a, $b) = @ret; | |
debug $fn->(@ret) if $GLOBAL{verbose} >= $level; | |
} | |
return @ret if wantarray; | |
$ret[0]; | |
} | |
sub totp { | |
my %opt = verbose { | |
"Generating token for:\n", pp({@_}) | |
} 2, @_; | |
my $tc = verbose { | |
"Using [$_] as counter." | |
} 2, int(($opt{time} - $opt{epoch}) / $opt{timestep}); | |
my $hmac = hmac_sha1 | |
scalar(reverse pack 'Q', $tc), | |
decode_base32($opt{secret}); | |
my @bytes = unpack 'C*', $hmac; | |
my $offset = $bytes[-1] & 0xF; | |
my $hash = (unpack 'N', pack 'C4', | |
verbose { | |
"Using [@{[join ',', @_]}] for the 4 bytes." | |
} 3, @bytes[$offset .. $offset + 3]) | |
& 0x7FFF_FFFF; | |
$opt{prefix} . verbose { "Generated [$_] for [$opt{name}]." } 2, | |
scalar reverse substr | |
scalar(reverse $hash), | |
0, | |
$opt{digits}; | |
} | |
sub check_initial_setup { | |
pod2usage | |
-exitval => 1, | |
-sections => 'INITIAL SETUP', | |
-verbose => 99 unless @_; | |
@_; | |
} | |
sub uniq_by(&;@) { | |
my ($fn, @list) = @_; | |
my %h = (); | |
grep { not $h{$fn->()}++ } @list; | |
} | |
GetOptions(\%GLOBAL, qw( | |
help|h+ | |
man|m+ | |
verbose|v+ | |
config|c=s@ | |
time|t=i | |
query|q=s | |
names|no-names|n:0 | |
add|a=s | |
)) or pod2usage 2; | |
# YAML::XS is so slow to load - 40ms minimum. | |
if ($ENV{YAML_XS}) { | |
require YAML::XS; | |
YAML::XS->import(qw(LoadFile)); | |
} | |
pod2usage 1 if $GLOBAL{help}; | |
pod2usage | |
-exitval => 0, | |
-verbose => 2 if $GLOBAL{man}; | |
if ($GLOBAL{verbose} > 0) { | |
require Data::Dump; | |
Data::Dump->import(qw(pp)); | |
} | |
my %DEFAULT = ( | |
epoch => 0, | |
digits => 6, | |
prefix => '', | |
timestep => 30, | |
time => $GLOBAL{time}, | |
); | |
my @DEFAULT_CONFIGS = ( | |
"$ENV{HOME}/.totp.yml", | |
"$ENV{XDG_CONFIG_HOME}/totp/totp.yml", | |
'./.totp.yml', | |
'./totp.yml', | |
); | |
sub config_files { | |
grep -r, map { | |
verbose { "Checking $_..." } | |
} @{$GLOBAL{config}} ? @{$GLOBAL{config}} : @DEFAULT_CONFIGS; | |
} | |
if ($GLOBAL{add} =~ m/^(?:([^:]+):)?(.+)$/) { | |
my $file = (config_files)[0] || $GLOBAL{config}[0] || $DEFAULT_CONFIGS[0]; | |
my $name = verbose { | |
"Adding token named [$_] with secret [$2] to [$file]." | |
} 1, $1 || 'DEFAULT'; | |
open my $fh, '>>', $file; | |
print $fh <<"END"; | |
--- # Added by $0 @{[join " ", @ARGV]} | |
name: $name | |
secret: $2 | |
END | |
exit 0 | |
} | |
my @generated = map { | |
$_->{token} = totp %DEFAULT, %$_; | |
$_; | |
} map { | |
verbose { "Loaded token:\n", pp($_) } 2 | |
} uniq_by { | |
$_->{secret} | |
} grep { | |
$_->{name} =~ $GLOBAL{query} | |
} map { | |
LoadFile $_ | |
} check_initial_setup map { | |
verbose { "Found file $_" } | |
} config_files; | |
# Print just the token if there's only | |
# one and STDOUT is not a TTY. (For copy/paste buffers.) | |
print $generated[0]{token} and exit 0 | |
if @generated == 1 and not -t *STDOUT; | |
say join "\n", map { | |
($GLOBAL{names} ? "$_->{name}: " : ()) . $_->{token}; | |
} @generated; | |
__END__ | |
=head1 NAME | |
I<totp> - Compute TOTP tokens | |
=head1 SYNOPSIS | |
totp [-hmnv] [-c FILES...] [-q REGEX] [-t TIME] | |
totp -a [NAME:]SECRET [-c FILE] | |
=head1 OPTIONS | |
-c FILES Look for token definitions in FILES | |
-q REGEX Only show tokens whose names match REGEX | |
-n Hide token names from output | |
-t TIME Use TIME as current time | |
-a SECRET Add the secret to the first config file found | |
-v Verbose output (to STDERR) | |
-h This text | |
-m Full manual page | |
=head1 DESCRIPTION | |
Implements the L<Time-based One-time Password | |
Algorithm|https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm> | |
Defaults for B<epoch>, B<time>, B<timestep>, and B<digits> are set | |
to the same values that L<Google | |
Authenticator|https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2> | |
uses. | |
=head1 INITIAL SETUP | |
You need to create at least one token definition file in | |
order for I<totp> to be useful. | |
Get your I<base32> encoded secret, SECRET, and then run: | |
$ totp -a SECRET | |
=head1 OPTION DETAILS | |
=over | |
=item B<--config|-c> FILES... | |
Read token definitions from I<FILES...> instead of the default | |
locations. | |
See L<LOCATIONS> for the default locations. | |
See L<FORMAT> for the format of the definition files. | |
=item B<--query|-q> REGEX | |
Only shows tokens whose B<name> property matches I<REGEX>. | |
Works well with B<--no-name> when you just need to echo a specific | |
token to C<STDOUT>. | |
=item B<--no-name|-n> | |
Show only the generated tokens. Do not show the B<name>s of the | |
tokens. | |
Works well with B<--query> when you just need to echo a specific | |
token to C<STDOUT>. | |
If only one token is defined and C<STDOUT> is not a TTY, B<--no-names> | |
is assumed. This is so you can do: | |
totp -q NAME | pbcopy | |
And have only the token itself in the paste buffer. | |
=item B<--time|-t> SECONDS | |
The Unix time to generate the token for in seconds since the Unix | |
epoch. | |
=item B<--add|-a> [NAME:]SECRET | |
Adds a basic token definition named I<NAME> with secret I<SECRET> | |
to either the first default configuration file (see L<LOCATIONS>) | |
or to the first file specified by a B<--config> argument. | |
=item B<--verbose|-v> | |
Increase output verbosity. Can be repeated for more verbosity. | |
=item B<--help|-h> | |
Show the L<SYNOPSIS> and L<OPTIONS> sections of this man page. | |
=item B<--man|-m> | |
Show this man page. | |
=back | |
=head1 CONFIGURATION FILES | |
I<totp> uses C<YAML> files to store token definitions. Their format | |
is defined in L<FORMAT> and the default locations that I<totp> looks | |
for configuration files in are specified in L<LOCATIONS>. | |
=head2 FORMAT | |
Each definition file may contain any number of C<YAML> documents. | |
Each document is assumed to define exactly one token. Each token | |
I<must> have the following properties defined: | |
=over | |
=item B<name> | |
The human-readable name of this token. These do not I<need> to be | |
unique, but you're just going to end up confusing yourself. | |
=item B<secret> | |
The I<base32>-encoded secret given to you by the token issuer. | |
=back | |
Each token I<may> also define the following properties: | |
=over | |
=item B<prefix> | |
String that will be prepended to the token after it is generated | |
but before it is echoed to the terminal. | |
Default: C<''> | |
=item B<timestep> | |
The amount of time in seconds between each new token generation. | |
Default: C<30> | |
=item B<digits> | |
The number of digits of the generated token to return. | |
Default: C<6> | |
=item B<epoch> | |
The starting unix time for generating the counter offset. | |
Default: C<0> | |
=back | |
=head2 LOCATIONS | |
I<totp> will look in these locations for token definition files, | |
in order: | |
$HOME/.totp.yml | |
$XDG_CONFIG_HOME/totp/totp.yml | |
./.totp.yml | |
./totp.yml | |
If you specify B<--config> arguments, they will replace the above | |
list in the order they are specified in the argument list. | |
=head1 ENVIRONMENT | |
If the I<YAML_XS> environment variable is set to any truthy value, | |
I<totp> will use L<YAML::XS> module to load token definitions. This | |
is slow - L<YAML::XS> takes ~40ms just to load. If this environment | |
variable is not set to a truthy value, I<totp> will instead use a | |
much faster line-based parser to read the definitions. | |
You should only need to set I<YAML_XS> if your definition file | |
contains something other than C<key: value> lines, trailing line | |
comments, and the C<---> document splitter. | |
=head1 EXAMPLE | |
If the contents of C<$HOME/.totp.yml> are: | |
--- | |
name: DEFAULT | |
secret: hq5sotirt722l3dmdyqvyhfxstmzoowb | |
Then: | |
$ totp -t 0 | |
DEFAULT: 657844 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment