Subversion Repositories web_pages

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
12 rodolico 1
#! /usr/bin/env perl
2
 
3
#    Copyright (c) 2025, Daily Data, Inc.
4
#
5
#    Redistribution and use in source and binary forms, with or without modification, 
6
#    are permitted provided that the following conditions are met:
7
#
8
#        Redistributions of source code must retain the above copyright notice, this
9
#          list of conditions and the following disclaimer.
10
#        Redistributions in binary form must reproduce the above copyright notice, 
11
#          this list of conditions and the following disclaimer in the documentation 
12
#           and/or other materials provided with the distribution.
13
#
14
#    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
15
#    ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
16
#    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
17
#    IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
18
#    INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
19
#    NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
20
#    OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
21
#    WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22
#    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
23
#    OF SUCH DAMAGE.
24
 
25
# script will read configuration file exported from opnSense 
26
# (System | Configuration | Backups | Download Configuration)
27
# and parse the users configuration. It will generate a png image of the QR code for the otp key,
28
# saving it on disk as routername_username.png, and store store the 
29
# routername/username/password/otp code and qr file name to a tab separated file named users.csv 
30
# ($csvFile hard coded below)
31
# if processing a router which already exists, will clean up first by removing all existing entries
32
# and qr files for that router.
33
#
34
# requires modules XML::Simple GD::Barcode::QRcode, which can be installed in Debian derivatives
35
# with 
36
# apt install libxml-simple-perl libgd-barcode-perl
37
# or via cpan with
38
# cpan XML::Simple GD::Barcode::QRcode
39
#
40
# Tested with
41
#    OPNSense 25.1.12
42
#    Microsoft Authenticator 6.2509.5952
43
#    FreeOTP v2.0.5
44
#
45
# Version 1.0.0  RWR 2025-09-21
46
#    initial release
47
 
48
use strict;
49
use warnings;
50
use XML::Simple;
51
use GD::Barcode::QRcode;
52
 
53
our $VERSION = '1.0.0';
54
 
55
my $csvFile = 'users.csv';
56
 
57
# Constants for creation of QR image
58
# this is used in URL created
59
my $issuer = "OPNsense";
60
# The size of individual "pixels" in graphics
61
my $moduleSize = 10;
62
 
63
#
64
# makeQR
65
#
66
# Creates a QR file with a name of ($routername_)$account.png
67
# where $routername is prepended if it is set and $account is the user name
68
# an underscore separates the routername and the account (if routername is set)
69
# the contents of the qr are made up of the constants 'otpauth://totp/' followed by $issuer
70
# and the parameters secret (otp code) and issuer again.
71
sub makeQR {
72
   my ( $routerName, $account, $secret, $issuer, $moduleSize ) = @_;
73
   my $otp_url = "otpauth://totp/$issuer:$account?secret=$secret&issuer=$issuer";
74
   # Ecc (Error Correction Code) can be one of Low, Medium, Quartile, and High
75
   # ModuleSize increases the actual size of each "pixel", enlarging for easier reading
76
   my $barcode = GD::Barcode::QRcode->new($otp_url, { Ecc => 'M', ModuleSize => $moduleSize } );
77
   my $image = $barcode->plot();
78
   # Save the image to a file. If there is a router name, prepend it to the filename with an underscore
79
   # allows the same user to be on two different routers
80
   my $outputFile = ($routerName ? $routerName . '_' : '' ) . $account . '.png';
81
   open my $out, '>', "$outputFile" or die "Cannot write file $outputFile: $!";
82
   binmode $out;
83
   print $out $image->png;
84
   close $out;
85
   return $outputFile;
86
}
87
 
88
# loads the CSV file into an array, removing any entry for $routerName and 
89
# removing all associated image files
90
sub loadCSV {
91
   my ( $filename, $routerName) = @_;
92
   my @data;
93
   if ( -e $filename ) { # if the file exists
94
      open DATA, "<$filename" or die "Could not read $filename: $!\n";
95
      my $line = <DATA>; # first line is the header
96
      chomp $line;
97
      push @data, $line; # preserve header
98
      while ( $line = <DATA> ) {
99
         chomp $line;
100
         # break apart so we can process fast
101
         my ($router, $name, $otp, $password, $filename ) = split( "\t", $line );
102
         # if this is the router we are working on, remove the filename associated
103
         if ( $router eq $routerName ) {
104
            if ( -e $filename ) {
105
               unlink( $filename );
106
            } else {
107
               warn "Can not unlink $filename, not found\n";
108
            }
109
         } else { # nope, not the current router, so just preserve the line
110
            push @data, $line;
111
         }
112
      }
113
   } else { # file doesn't exist, so just create empty list with header
114
      warn "$filename does not exist, creating\n";
115
      push @data, "router\tname\totp\tpassword\tfilename";
116
   }
117
   return \@data;
118
}
119
 
120
 
121
# Load the XML file into 
122
my $xmlFileName = shift;
123
my $routerName = shift;
124
die "Usage: $0 configfilename [routername]\n" unless $xmlFileName && $routerName;
125
#warn "Not using separate router names\n" unless $routerName;
126
my $xml = XML::Simple->new();
127
 
128
# Parse the XML file into a hash
129
my $data = $xml->XMLin($xmlFileName, ForceArray => 0, KeyAttr => []);
130
# we only want the user data, which is an array ref under system->user
131
my $users = $data->{'system'}->{'user'};
132
 
133
# if there is a router name, prepend to the csv file name
134
# $csvFile = $routerName . '_' . $csvFile if $routerName;
135
 
136
my $output = &loadCSV( $csvFile, $routerName );
137
 
138
# process each element in the array. Each array element is a hashref containing the information we need
139
foreach my $user ( @$users ) {
140
   if ( ref( $user->{'otp_seed'} ) ) { # otp_seed is a ref to an empty hash if no key has been defined
141
      # that is not an error, as some accounts do not have access
142
      warn "$user->{name} has no otp seed\n";
143
   } else { # this is a user with an otp seed
144
      # create the QR File and get the name of it into $filename
145
      my $filename = makeQR( $routerName, $user->{'name'}, $user->{'otp_seed'}, $issuer, $moduleSize );
146
      # add a row to the CSV
147
      push @$output,  join( "\t", ( $routerName, $user->{'name'}, $user->{'otp_seed'}, $user->{'password'}, $filename ) );
148
   }
149
}
150
# store data in a tab separated value file
151
open USERS,">$csvFile" or die "Could not create $csvFile: $!\n";
152
# put in a header so we can find things easily
153
print USERS join( "\n", @$output );
154
close USERS;
155
 
156
#use Data::Dumper;
157
# print Dumper( $users );