+sub oath_verify_otp {
+ my ($otp, $keys, $step, $digits) = @_;
+
+ die "oath: missing password\n" if !defined($otp);
+ die "oath: no associated oath keys\n" if $keys =~ m/^\s+$/;
+
+ $step = 30 if !$step;
+ $digits = 6 if !$digits;
+
+ my $found;
+ foreach my $k (PVE::Tools::split_list($keys)) {
+ # Note: we generate 3 values to allow small time drift
+ my $binkey;
+ if ($k =~ /^[A-Z2-7=]{16}$/) {
+ $binkey = MIME::Base32::decode_rfc3548($k);
+ } elsif ($k =~ /^[A-Fa-f0-9]{40}$/) {
+ $binkey = pack('H*', $k);
+ } else {
+ die "unrecognized key format, must be hex or base32 encoded\n";
+ }
+
+ # force integer division for time/step
+ use integer;
+ my $now = time()/$step - 1;
+ $found = 1 if $otp eq hotp($binkey, $now+0, $digits);
+ $found = 1 if $otp eq hotp($binkey, $now+1, $digits);
+ $found = 1 if $otp eq hotp($binkey, $now+2, $digits);
+ last if $found;
+ }
+
+ die "oath auth failed\n" if !$found;
+}
+
+# bash completion helpers
+
+sub complete_username {
+
+ my $user_cfg = cfs_read_file('user.cfg');
+
+ return [ keys %{$user_cfg->{users}} ];
+}
+
+sub complete_group {
+
+ my $user_cfg = cfs_read_file('user.cfg');
+
+ return [ keys %{$user_cfg->{groups}} ];
+}
+
+sub complete_realm {
+
+ my $domain_cfg = cfs_read_file('domains.cfg');
+
+ return [ keys %{$domain_cfg->{ids}} ];
+}
+