Add script to encrypt existing TOTP secrets (#2625)
This commit is contained in:
parent
09126d91fd
commit
6a40a70ddb
|
@ -0,0 +1,314 @@
|
|||
#!/usr/bin/perl
|
||||
#
|
||||
use warnings;
|
||||
use strict;
|
||||
use POSIX;
|
||||
use Lemonldap::NG::Common::Conf;
|
||||
use Lemonldap::NG::Common::Apache::Session;
|
||||
use Lemonldap::NG::Common::Session;
|
||||
use Lemonldap::NG::Common::TOTP;
|
||||
use JSON qw(from_json to_json);
|
||||
use Getopt::Long qw(:config auto_help);
|
||||
use Pod::Usage;
|
||||
|
||||
my ( $dryrun, $verbose, $force, $update, $oldkey, $newkey );
|
||||
GetOptions(
|
||||
"n|dry-run" => \$dryrun,
|
||||
"v|verbose" => \$verbose,
|
||||
"f|force" => \$force,
|
||||
"u|update" => \$update,
|
||||
"o|old-key=s" => \$oldkey,
|
||||
"k|new-key=s" => \$newkey,
|
||||
);
|
||||
|
||||
eval {
|
||||
POSIX::setgid( scalar( getgrnam('www-data') ) );
|
||||
POSIX::setuid( scalar( getpwnam('www-data') ) );
|
||||
};
|
||||
|
||||
sub verbose {
|
||||
print STDERR @_, "\n" if $verbose;
|
||||
}
|
||||
|
||||
sub info {
|
||||
print STDERR @_, "\n";
|
||||
}
|
||||
|
||||
# Get config
|
||||
my $res = Lemonldap::NG::Common::Conf->new();
|
||||
die $Lemonldap::NG::Common::Conf::msg unless ($res);
|
||||
my $conf = $res->getConf();
|
||||
|
||||
my $localconf = $res->getLocalConf()
|
||||
or die "Unable to get local configuration ($!)";
|
||||
|
||||
if ($localconf) {
|
||||
$conf->{$_} = $localconf->{$_} foreach ( keys %$localconf );
|
||||
}
|
||||
|
||||
if ( !$conf->{totp2fEncryptSecret} ) {
|
||||
if ( !$force ) {
|
||||
die "Encryption of TOTP secrets is not enabled in configuration."
|
||||
. " Use --force to ignore this error";
|
||||
}
|
||||
}
|
||||
|
||||
# Create TOTP object
|
||||
|
||||
my $decrypt_totp = Lemonldap::NG::Common::TOTP->new(
|
||||
key => ( $oldkey || $conf->{totp2fKey} || $conf->{key} ),
|
||||
encryptSecret => 0,
|
||||
);
|
||||
|
||||
my $encrypt_totp = Lemonldap::NG::Common::TOTP->new(
|
||||
key => ( $newkey || $conf->{totp2fKey} || $conf->{key} ),
|
||||
encryptSecret => ( ( $newkey || "" ) eq "DECRYPT" ? 0 : 1 ),
|
||||
);
|
||||
|
||||
# Search psessions
|
||||
my $args;
|
||||
if ( $conf->{"persistentStorage"} ) {
|
||||
$args = $conf->{"persistentStorageOptions"};
|
||||
$args->{backend} = $conf->{"persistentStorage"};
|
||||
}
|
||||
else {
|
||||
$args = $conf->{"globalStorageOptions"};
|
||||
$args->{backend} = $conf->{"globalStorage"};
|
||||
}
|
||||
|
||||
verbose "Searching for persistent sessions";
|
||||
$res = Lemonldap::NG::Common::Apache::Session->searchOn( $args, '_session_kind',
|
||||
'Persistent', '_2fDevices', '_session_uid' );
|
||||
|
||||
if ( ref($res) eq "HASH" ) {
|
||||
verbose "Found " . scalar( keys %{$res} ) . " persistent sessions";
|
||||
|
||||
# For each found psession
|
||||
for my $k ( keys %{$res} ) {
|
||||
my $_2fDevices = $res->{$k}->{_2fDevices};
|
||||
my $uid = $res->{$k}->{_session_uid};
|
||||
verbose "Processing psession $k for user $uid";
|
||||
encrypt_session( $k, $uid, $_2fDevices );
|
||||
}
|
||||
}
|
||||
else {
|
||||
die "Could not find any persistent sessions";
|
||||
}
|
||||
|
||||
sub encrypt_session {
|
||||
my ( $k, $uid, $_2fDevices ) = @_;
|
||||
|
||||
eval {
|
||||
# parse _2fDevices if found
|
||||
if ($_2fDevices) {
|
||||
$_2fDevices = from_json($_2fDevices);
|
||||
|
||||
# If the session has 2f devices
|
||||
if ( ref($_2fDevices) eq "ARRAY" and @{$_2fDevices} > 0 ) {
|
||||
my $changed = convert_keys_for_user( $uid, $_2fDevices );
|
||||
if ( $changed and !$dryrun ) {
|
||||
eval { update2fArray( $k, $_2fDevices ); };
|
||||
if ($@) {
|
||||
info "Error updating session for $uid: $@";
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
verbose "User $uid does not have a TOTP";
|
||||
}
|
||||
}
|
||||
else {
|
||||
verbose "User $uid does not have a TOTP";
|
||||
}
|
||||
|
||||
};
|
||||
if ($@) {
|
||||
verbose "Error on psession $k: $@";
|
||||
}
|
||||
}
|
||||
|
||||
sub update2fArray {
|
||||
my ( $id, $_2fDevices ) = @_;
|
||||
|
||||
my $session = Lemonldap::NG::Common::Session->new(
|
||||
storageModule => $args->{backend},
|
||||
storageModuleOptions => $args,
|
||||
id => $id,
|
||||
);
|
||||
|
||||
unless ( $session->data ) {
|
||||
die "Error while opening session $id";
|
||||
}
|
||||
|
||||
unless ( $session->update( { _2fDevices => to_json($_2fDevices) } ) ) {
|
||||
die "Error while updating session $id";
|
||||
}
|
||||
}
|
||||
|
||||
sub convert_device_for_user {
|
||||
my ( $uid, $device ) = @_;
|
||||
my $changed = 0;
|
||||
|
||||
# In update mode, decrypt then encrypt
|
||||
if ($update) {
|
||||
my $cleartext_secret =
|
||||
$decrypt_totp->get_cleartext_secret( $device->{_secret} );
|
||||
if ($cleartext_secret) {
|
||||
my $newsecret =
|
||||
$encrypt_totp->get_storable_secret($cleartext_secret);
|
||||
$device->{_secret} = $newsecret;
|
||||
$changed = 1;
|
||||
verbose 'Updated secret for ' . $uid;
|
||||
}
|
||||
else {
|
||||
info 'Unable to decrypt TOTP secret for ' . $uid;
|
||||
}
|
||||
|
||||
# In normal mode, only encrypt non-encrypted secrets
|
||||
}
|
||||
else {
|
||||
if ( !$encrypt_totp->is_encrypted( $device->{_secret} ) ) {
|
||||
$device->{_secret} =
|
||||
$encrypt_totp->get_storable_secret( $device->{_secret} );
|
||||
$changed = 1;
|
||||
info 'Encrypted TOTP secret for ' . $uid;
|
||||
}
|
||||
else {
|
||||
verbose 'Secret is already encrypted';
|
||||
}
|
||||
}
|
||||
return $changed;
|
||||
}
|
||||
|
||||
sub convert_keys_for_user {
|
||||
my ( $uid, $devices ) = @_;
|
||||
my $has_totp = 0;
|
||||
my $changed = 0;
|
||||
for my $device ( @{$devices} ) {
|
||||
if ( $device->{type} eq "TOTP" ) {
|
||||
$has_totp = 1;
|
||||
my $epoch = $device->{epoch};
|
||||
verbose "Processing device with epoch $epoch for user $uid";
|
||||
my $changed_current = convert_device_for_user( $uid, $device );
|
||||
$changed = 1 if $changed_current;
|
||||
}
|
||||
}
|
||||
|
||||
if ( !$has_totp ) {
|
||||
verbose "User $uid does not have a TOTP";
|
||||
}
|
||||
return ( $has_totp, $changed );
|
||||
}
|
||||
|
||||
__END__
|
||||
|
||||
=encoding utf8
|
||||
=head1 NAME
|
||||
|
||||
encryptTotpSecret - A tool to encrypt existing TOTP secrets
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
encryptTotpSecret [options]
|
||||
|
||||
Options:
|
||||
-h, --help Show help message
|
||||
-n, --dry-run Do not perform any operation
|
||||
-u, --update Re-encrypt or decrypt already encrypted data
|
||||
-o, --old-key Specify old key when re-encrypting
|
||||
-k, --new-key Specify new key when re-encrypting or decrypting
|
||||
-f, --force Ignore configuration errors
|
||||
-v, --verbose Print additional information
|
||||
|
||||
This script is a migration tool that you can use after enabling TOTP secret
|
||||
encryption in the Manager. It will make sure that existing secrets are
|
||||
encrypted, and not just newly registered secrets.
|
||||
|
||||
=head1 OPTIONS
|
||||
|
||||
=over 8
|
||||
|
||||
=item B<--help>, B<-h>
|
||||
|
||||
Print a brief help message and exit.
|
||||
|
||||
=item B<--dry-run>, B<-n>
|
||||
|
||||
Prevent the script from saving modifications to the session database
|
||||
|
||||
=item B<--update>, B<-u>
|
||||
|
||||
By default, secrets that are already in encrypted form are skipped by the
|
||||
script. Use this option to force already encrypted secrets to be decrypted,
|
||||
then re-encrypted using a different key (or decrypted)
|
||||
|
||||
=item B<--old-key>, B<-o>
|
||||
|
||||
The key used to decrypt secrets in B<--update> mode.
|
||||
|
||||
By default, the B<totp2fKey> or B<key> LemonLDAP::NG configuration parameters
|
||||
are used.
|
||||
|
||||
=item B<--new-key>, B<-k>
|
||||
|
||||
The key used to encrypt secrets. Use B<-u -k DECRYPT> to decrypt secrets instead.
|
||||
|
||||
By default, the B<totp2fKey> or B<key> LemonLDAP::NG configuration parameters
|
||||
are used.
|
||||
|
||||
=item B<--force>, B<-f>
|
||||
|
||||
Encrypt existing TOTP secrets even if encryption is disabled in the configuration
|
||||
|
||||
=item B<--verbose>, B<-v>
|
||||
|
||||
Increase the level of details provided by the script
|
||||
|
||||
=back
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<http://lemonldap-ng.org/>
|
||||
|
||||
=head1 AUTHORS
|
||||
|
||||
=over
|
||||
|
||||
=item Maxime Besson, E<lt>maxime.besson@worteks.comE<gt>
|
||||
|
||||
=back
|
||||
|
||||
=head1 BUG REPORT
|
||||
|
||||
Use OW2 system to report bug or ask for features:
|
||||
L<https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues>
|
||||
|
||||
=head1 DOWNLOAD
|
||||
|
||||
Lemonldap::NG is available at
|
||||
L<https://lemonldap-ng.org/download>
|
||||
|
||||
=head1 COPYRIGHT AND LICENSE
|
||||
|
||||
=over
|
||||
|
||||
=item Copyright (C) 2008-2016 by Xavier Guimard, E<lt>x.guimard@free.frE<gt>
|
||||
|
||||
=item Copyright (C) 2008-2016 by Clément Oudot, E<lt>clem.oudot@gmail.comE<gt>
|
||||
|
||||
=back
|
||||
|
||||
This library 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, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program 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.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see L<http://www.gnu.org/licenses/>.
|
||||
|
||||
=cut
|
Loading…
Reference in New Issue