From 6a40a70ddb19fe30d9e8ff8cd7f7d3c8caf02d89 Mon Sep 17 00:00:00 2001 From: Maxime Besson Date: Mon, 27 Sep 2021 17:47:08 +0200 Subject: [PATCH] Add script to encrypt existing TOTP secrets (#2625) --- .../scripts/encryptTotpSecrets | 314 ++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 lemonldap-ng-common/scripts/encryptTotpSecrets diff --git a/lemonldap-ng-common/scripts/encryptTotpSecrets b/lemonldap-ng-common/scripts/encryptTotpSecrets new file mode 100644 index 000000000..a7b552846 --- /dev/null +++ b/lemonldap-ng-common/scripts/encryptTotpSecrets @@ -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 or B 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 or B 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 + +=head1 AUTHORS + +=over + +=item Maxime Besson, Emaxime.besson@worteks.comE + +=back + +=head1 BUG REPORT + +Use OW2 system to report bug or ask for features: +L + +=head1 DOWNLOAD + +Lemonldap::NG is available at +L + +=head1 COPYRIGHT AND LICENSE + +=over + +=item Copyright (C) 2008-2016 by Xavier Guimard, Ex.guimard@free.frE + +=item Copyright (C) 2008-2016 by Clément Oudot, Eclem.oudot@gmail.comE + +=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. + +=cut