mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-02-08 11:16:56 +00:00
5947b26a8f
git-svn-id: https://svn.fhem.de/fhem/trunk@23628 2b470e98-0d58-463d-a4d8-8e2adae1ed80
403 lines
13 KiB
Perl
403 lines
13 KiB
Perl
# $Id$
|
|
|
|
# License & technical informations
|
|
=for comment
|
|
#
|
|
################################################################
|
|
#
|
|
# This script is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# The GNU General Public License can be found at
|
|
# http://www.gnu.org/copyleft/gpl.html.
|
|
# A copy is found in the textfile GPL.txt and important notices to the license
|
|
# from the author is found in LICENSE.txt distributed with these scripts.
|
|
#
|
|
# This script is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# This copyright notice MUST APPEAR in all copies of the script!
|
|
#
|
|
# Homepage: http://www.fhem.de
|
|
#
|
|
################################################################
|
|
#
|
|
# Module 98_GoogleAuth.pm
|
|
# written by pandabaer_de, revised by betateilchen
|
|
#
|
|
# based on informations from this website
|
|
# https://blog.darkpan.com/article/6/Perl-and-Google-Authenticator.html
|
|
#
|
|
# requires additional perl modules
|
|
# Convert::Base32 Authen::OATH Crypt::URandom
|
|
#
|
|
# on Debian systems, use apt-get to install appropriate packages
|
|
# libconvert-base32-perl libauthen-oath-perl libcrypt-urandom-perl
|
|
#
|
|
################################################################
|
|
#
|
|
=cut
|
|
|
|
# Development history
|
|
=for comment
|
|
#
|
|
# 2017-01-15 - first commit to ./contrib
|
|
#
|
|
# 2017-01-15 - added: direct QR display after set
|
|
# added: attribute ga_qrSize
|
|
# added: FW_detailFn
|
|
# added: attribute ga_labelName
|
|
# added: reading lastCheck
|
|
# removed: reading qr_url
|
|
# added: show link to qrcode and key for manual use
|
|
# in device details
|
|
# added: set command "revoke" to prevent overwrite
|
|
# of existing key
|
|
# added: attribute ga_showKey
|
|
# attribute ga_showLink
|
|
# added: function gAuth(<device>,<token>) for easy use
|
|
# added: FW_summaryFn
|
|
# added: commandref documentation EN
|
|
#
|
|
# 2017-01-15 - published to FHEM
|
|
# fixed: problem on iOS if label contains spaces
|
|
# added: issuer=FHEM in qr-code
|
|
#
|
|
# 2017-01-16 - added: attributes ga_showQR, ga_strictCheck
|
|
# removed: FW_summaryFn (not really useful)
|
|
#
|
|
# 2020-03-31 - changed: remove prototyping and undef results
|
|
#
|
|
# 2021-01-26 - changed: rework code to use own package
|
|
#
|
|
=cut
|
|
|
|
package FHEM::GoogleAuth; ## no critic
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Convert::Base32;
|
|
use Authen::OATH;
|
|
use URI::Escape;
|
|
use Crypt::URandom qw( urandom );
|
|
|
|
use GPUtils qw(GP_Import GP_Export);
|
|
|
|
## Import der FHEM Funktionen
|
|
|
|
BEGIN {
|
|
# Import from main context
|
|
GP_Import(
|
|
qw( AttrVal
|
|
Debug
|
|
Log3
|
|
defs
|
|
readingFnAttributes
|
|
readingsSingleUpdate
|
|
setKeyValue
|
|
getKeyValue )
|
|
);
|
|
|
|
#-- Export to main context with different name
|
|
GP_Export(
|
|
qw( Initialize )
|
|
);
|
|
}
|
|
|
|
sub Initialize {
|
|
my ($hash) = @_;
|
|
|
|
$hash->{DefFn} = \&Define;
|
|
$hash->{DeleteFn} = \&Delete;
|
|
$hash->{SetFn} = \&Set;
|
|
$hash->{GetFn} = \&Get;
|
|
$hash->{FW_detailFn} = \&Detail;
|
|
|
|
$hash->{AttrList} = "ga_labelName ".
|
|
"ga_qrSize:100x100,200x200,300x300,400x400 ".
|
|
"ga_showKey:0,1 ga_showLink:0,1 ga_showQR:1,0 ".
|
|
"ga_strictCheck:0,1 ".
|
|
"$readingFnAttributes";
|
|
}
|
|
|
|
sub Define {
|
|
my ($hash, $def) = @_;
|
|
my $name = $hash->{NAME};
|
|
my @a = split("[ \t][ \t]*", $def);
|
|
return "Usage: Use Google Authenticator" if(@a != 2);
|
|
|
|
Log3($hash,4,"googleAuth $name: defined");
|
|
readingsSingleUpdate($hash,'state','defined',1);
|
|
return;
|
|
}
|
|
|
|
sub Delete {
|
|
my ($hash,$name) = @_;
|
|
setKeyValue("googleAuth$name",undef);
|
|
}
|
|
|
|
sub Set {
|
|
my ($hash, $name, $cmd, @args) = @_;
|
|
my $usage = "Unknown argument, choose one of new:noArg revoke:noArg";
|
|
|
|
if($cmd eq "new") {
|
|
#SOURCE: https://blog.darkpan.com/article/6/Perl-and-Google-Authenticator.html
|
|
return "Please revoke existing key first!" if defined(getKeyValue("googleAuth$name"));
|
|
my $secret_bytes = urandom(50);
|
|
my $secret_base32 = encode_base32( $secret_bytes );
|
|
Log3($hash,5,"googleAuth $name: secret_bytes=$secret_bytes");
|
|
Log3($hash,5,"googleAuth $name: set secret_base32=$secret_base32");
|
|
setKeyValue("googleAuth$name",$secret_base32); # write to fhem keystore
|
|
readingsSingleUpdate($hash,'state','active',1);
|
|
} elsif ($cmd eq "revoke") {
|
|
setKeyValue("googleAuth$name",undef);
|
|
readingsSingleUpdate($hash,'state','defined',1);
|
|
} else {
|
|
return $usage
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub Get {
|
|
my ($hash, $name, $cmd, $given_token) = @_;
|
|
my $usage = "Unknown argument, choose one of check";
|
|
|
|
if ($cmd eq "check") {
|
|
return "Token missing!" unless (defined($given_token) && $given_token);
|
|
$given_token = _ga_make_token_6($given_token);
|
|
Log3($hash,4,"googleAuth $name: given: $given_token");
|
|
|
|
my $secret_base32 = getKeyValue("googleAuth$name"); # read from fhem keystore
|
|
Log3($hash,5,"googleAuth $name: get secret_base32=$secret_base32");
|
|
$secret_base32 = decode_base32($secret_base32);
|
|
Log3($hash,5,"googleAuth $name: secret_bytes=$secret_base32");
|
|
|
|
my $oath = Authen::OATH->new;
|
|
my @possible;
|
|
if (AttrVal($name,'ga_strictCheck',0) == 1) {
|
|
@possible = _ga_make_token_6($oath->totp($secret_base32));
|
|
} else {
|
|
@possible = map { _ga_make_token_6($oath->totp($secret_base32, $_)) } time-30, time, time+30;
|
|
}
|
|
Log3($hash,4,"googleAuth $name: possible: ".join ' ',@possible);
|
|
|
|
my $result = (grep /^$given_token$/, @possible) ? 1 : -1;
|
|
readingsSingleUpdate($hash,'lastResult',$result,0);
|
|
Log3($hash,4,"googleAuth $name: result: $result");
|
|
return $result;
|
|
}
|
|
return $usage;
|
|
}
|
|
|
|
sub Detail {
|
|
my ($FW_wname, $name, $room, $pageHash) = @_;
|
|
my $qr_url = _ga_make_url($name);
|
|
my $secret_base32 = getKeyValue("googleAuth$name"); # read from fhem keystore
|
|
return unless defined($qr_url);
|
|
my $ret = "<table>";
|
|
$ret .= "<tr><td rowspan=2>";
|
|
$ret .= "<a href=\"$qr_url\"><img src=\"$qr_url\"><\/a>"
|
|
if AttrVal($name,'ga_showQR',1);
|
|
$ret .= "</td>";
|
|
$ret .= "<td><br> <a href=\"$qr_url\">Link to QR code<\/a><\/td>"
|
|
if AttrVal($name,'ga_showLink',0);
|
|
$ret .= "</tr>";
|
|
$ret .= "<tr><td> Key (for manual use):<br> $secret_base32</td><tr>"
|
|
if AttrVal($name,'ga_showKey',0);
|
|
$ret .= "</table>";
|
|
return $ret;
|
|
}
|
|
|
|
# helper functions
|
|
sub _ga_make_url {
|
|
my ($name) = @_;
|
|
my $label = AttrVal($name,'ga_labelName',"FHEM Authentication $name");
|
|
$label =~ s/\s/\%20/g;
|
|
my $qrsize = AttrVal($name,'ga_qrSize','200x200');
|
|
my $secret_base32 = getKeyValue("googleAuth$name");
|
|
return unless defined($secret_base32);
|
|
my $url = "otpauth://totp/$label?secret=$secret_base32&issuer=FHEM";
|
|
my $qr_url = "https://chart.googleapis.com/chart?cht=qr&chs=$qrsize"."&chl=";
|
|
$qr_url .= uri_escape($url);
|
|
return $qr_url;
|
|
}
|
|
|
|
sub _ga_make_token_6 {
|
|
my $token = shift;
|
|
while (length $token < 6) {
|
|
$token = "0$token";
|
|
}
|
|
return $token;
|
|
}
|
|
|
|
package main;
|
|
|
|
sub gAuth {
|
|
my($name,$token,$aDev) = @_;
|
|
return "missing name!" unless $name;
|
|
return "missing $token!" unless $token;
|
|
$aDev //= '';
|
|
my $myHash = $defs{$name};
|
|
if (exists $myHash->{helper}{$token}
|
|
&& $myHash->{helper}{$token} - gettimeofday() > 0
|
|
&& defined($aDev)
|
|
&& exists $defs{$aDev}
|
|
&& $defs{$aDev}{TYPE} eq 'allowed') {
|
|
return 1;
|
|
} else {
|
|
delete $myHash->{helper}{$token} if defined($myHash->{helper}{$token});
|
|
my $result = FHEM::GoogleAuth::Get($myHash,$name,'check',$token);
|
|
if ($result == 1) {$myHash->{helper}{$token} = gettimeofday()+DAYSECONDS};
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
1;
|
|
|
|
=pod
|
|
=item helper
|
|
=item summary Module to use GoogleAuthenticator
|
|
=item summary_DE Modul zur Nutzung von GoogleAuthenticator
|
|
=begin html
|
|
|
|
<a name="GoogleAuth"></a>
|
|
<h3>GoogleAuth</h3>
|
|
|
|
<ul>
|
|
GoogleAuthenticator provides two-factor-authentication using one-time-passwords (token).<br/>
|
|
These tokens are generated using the mobile app „Google Authenticator“ for example on a smartphone.<br/>
|
|
See <a href="https://en.wikipedia.org/wiki/Google_Authenticator">https://en.wikipedia.org/wiki/Google_Authenticator</a>
|
|
for more informations.<br/>
|
|
<br/>
|
|
<br/>
|
|
<b>Prerequesits</b><br/>
|
|
<br/>
|
|
|
|
<li>The fhem implementation of the Google Authenticator is credited to the following publication:<br/>
|
|
<a href="https://blog.darkpan.com/article/6/Perl-and-Google-Authenticator.html">https://blog.darkpan.com/article/6/Perl-and-Google-Authenticator.html</a></li>
|
|
<br/>
|
|
|
|
<li>Module uses following additional Perl modules:<br/>
|
|
<br/>
|
|
<ul><code>Convert::Base32 Authen::OATH Crypt::URandom</code></ul>
|
|
<br/>
|
|
If not already installed in your environment, please install them using appropriate commands from your environment.<br/>
|
|
<br/>
|
|
Package installation in debian environments:<br/>
|
|
<br/>
|
|
<ul><code>apt-get install libconvert-base32-perl libauthen-oath-perl libcrypt-urandom-perl</code></ul></li>
|
|
<br/>
|
|
<br/>
|
|
|
|
<a name="GoogleAuthdefine"></a>
|
|
<b>Define</b><br/><br/>
|
|
<ul>
|
|
<code>define <name> GoogleAuth</code><br/>
|
|
<br/>
|
|
Example:<br/><br/>
|
|
<ul><code>define googleAuth GoogleAuth</code><br/></ul>
|
|
</ul>
|
|
<br/>
|
|
<br/>
|
|
|
|
<a name="GoogleAuthset"></a>
|
|
<b>Set Commands</b><br/><br/>
|
|
<ul>
|
|
<li><code>set <name> new</code><br/>
|
|
<br/>
|
|
Generates a new secret key and displays the corresponding QR image.<br/>
|
|
Using the photo function of the Google Authenticator app,<br/>
|
|
this QR image can be used to transfer the secret key to the app.
|
|
</li>
|
|
<br/>
|
|
<li><code>set <name> revoke</code><br/>
|
|
<br/>
|
|
Remove existing key.<br/>
|
|
<b>You can not create a new key before</b> an existing key was deleted.<br/>
|
|
</li>
|
|
</ul>
|
|
<br/>
|
|
<br/>
|
|
|
|
<a name="GoogleAuthget"></a>
|
|
<b>Get Commands</b><br/><br/>
|
|
<ul>
|
|
<li><code>get <name> check <token></code><br/>
|
|
<br/>
|
|
Check the validity of a given token; return value is 1 for a valid token, otherwise -1.<br/>
|
|
<ul>
|
|
<li>Token always consists of six numerical digits and will change every 30 seconds.</li>
|
|
<li>Token is valid if it matches one of three tokens calculated by FHEM<br/>
|
|
using three timestamps: -30 seconds, now and +30 seconds.<br/>
|
|
This behavior can be changed by attribute ga_strictCheck.</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
<br/>
|
|
<br/>
|
|
|
|
<a name="GoogleAuthattr"></a>
|
|
<b>Attributes</b><br/><br/>
|
|
<ul>
|
|
<li><b>ga_labelName</b> - define a Name to identify PassCode inside the app.<br/>
|
|
<b>Do not use any special characters,</b> except SPACE, in this attribute!</li>
|
|
<li><b>ga_qrSize</b> - select image size of qr code</li>
|
|
<li><b>ga_showKey</b> - show key for manual use if set to 1</li>
|
|
<li><b>ga_showLink</b> - show link to qr code if set to 1</li>
|
|
<li><b>ga_showQR</b> - show qr code if set to 1</li>
|
|
<li><b>ga_strictCheck</b><br/>
|
|
AttrVal = 1 : check given token against one token<br/>
|
|
AttrVal = 0 : check given token against three tokens(default)</li>
|
|
</ul>
|
|
<br/>
|
|
<br/>
|
|
|
|
<a name="GoogleAuthreadings"></a>
|
|
<b>Generated Readings/Events</b><br/><br/>
|
|
<ul>
|
|
<li><b>lastResult</b> - contains result from last token check</li>
|
|
<li><b>state</b> - "active" if a key is set, otherwise "defined"</li>
|
|
</ul>
|
|
<br/>
|
|
<br/>
|
|
|
|
<b>Integration with gAuth()</b><br/><br/>
|
|
<ul>
|
|
<li><code>gAuth(<name>,<token>)</code><br/>
|
|
<br/>
|
|
For easy use in your own scenarios you can call function gAuth(),<br/>
|
|
which will return same result codes as the "get" command.
|
|
</li>
|
|
<br/>
|
|
<li>Usage of gAuth() for login to FHEM<br/>
|
|
<br/>
|
|
A device of TYPE=allowed can be used to secure login to FHEM via basicAuth.<br/>
|
|
<code>attr <deviceName> basicAuth { "$user" eq "xxx" && gAuth("GoogleAuth","$password","<deviceName>") == 1 }</code><br/>
|
|
<br/>
|
|
In both cases <deviceName> has to be the name of the allowed-TYPE device.<br/>
|
|
The authenticated login will be cached for this device for a maximum period of 86400 seconds.<br/>
|
|
Do not use basicAuthExpiry in this scenario!<br/>
|
|
The cache will be lost after FHEM restart or after any invalid token checked by the named allowed-TYPE device.<br/>
|
|
</li>
|
|
</ul>
|
|
<br/>
|
|
<br/>
|
|
|
|
</ul>
|
|
=end html
|
|
=begin html_DE
|
|
|
|
<a name="GoogleAuth"></a>
|
|
<h3>GoogleAuth</h3>
|
|
<ul>
|
|
Sorry, keine deutsche Dokumentation vorhanden.<br/><br/>
|
|
Die englische Doku gibt es hier: <a href='commandref.html#GoogleAuth'>GoogleAuth</a><br/>
|
|
</ul>
|
|
=end html_DE
|
|
|
|
=cut |