mirror of
https://github.com/webmin/webmin.git
synced 2026-05-07 07:40:28 +01:00
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
Webmin now uses the bundled QRCode::Encoder implementation by default to generate TOTP QR codes locally and inline, without relying on qr.cgi or external services. This encoder requires Perl 5.24 or newer, so qrencode is no longer included as a recommended package dependency. On older systems where the bundled encoder cannot run, admins can still install qrencode manually to restore QR generation support. Systems old enough to lack Perl 5.24 are typically already well past their supported security lifecycle, so keeping qrencode preinstalled by default is no longer treated as a packaging requirement. When neither option is available, Webmin falls back cleanly to manual TOTP setup using the shared secret. https://github.com/webmin/webmin/issues/2667#issuecomment-4247431279 [no-build]
253 lines
7.2 KiB
Perl
253 lines
7.2 KiB
Perl
package QRCode::Encoder;
|
|
use v5.24;
|
|
use warnings;
|
|
use experimental qw< signatures >;
|
|
{ our $VERSION = '0.005' }
|
|
|
|
use Math::ReedSolomon::Encoder qw< rs_correction_string >;
|
|
use QRCode::Encoder::QRSpec qw< :all >;
|
|
use QRCode::Encoder::Matrix qw< add_matrix >;
|
|
use Exporter qw< import >;
|
|
our @EXPORT_OK = qw<
|
|
qr_best_params
|
|
qr_encode
|
|
qr_mode
|
|
>;
|
|
our %EXPORT_TAGS = (all => [ @EXPORT_OK ]);
|
|
|
|
sub qr_mode ($octets) {
|
|
return 'numeric' if $octets =~ m{\A \d* \z}mxs;
|
|
return 'alphanumeric'
|
|
if $octets =~ m{\A [0-9A-Z\x20\x24\x25\x2a\x2b\x2d-\x2f\x3a]+ \z}mxs;
|
|
return 'kanji'
|
|
if $octets =~ m{\A
|
|
(?: # start of a pair
|
|
(?: [\x81-\x9f\xe0-\xea] [\x40-\x7e\x80-\xfc])
|
|
| (?: \xeb [\x40-\x7e\x80\xbf])
|
|
)+
|
|
\z}mxs;
|
|
return 'byte';
|
|
}
|
|
|
|
sub qr_best_params (@args) {
|
|
state $rank_for = { L => 1, M => 2, Q => 3, H => 4 };
|
|
my %args = scalar(@args) % 2 ? (octets => @args) : @args;
|
|
my $size = length($args{octets});
|
|
my $mode = $args{mode} // qr_mode($args{octets});
|
|
|
|
my $version = $args{version};
|
|
my $min_version = $args{'min-version'} // 1;
|
|
$version //= 40 if $min_version eq 40;
|
|
|
|
my $level = $args{level};
|
|
my $min_level = $args{min_level} // 'L';
|
|
$level //= 'H' if $min_level eq 'H';
|
|
|
|
if (defined($level)) {
|
|
my $minv = qrspec_min_version_for($mode, $size, $level)
|
|
or die "no suitable version for $mode/$size/$level";
|
|
if (defined($version)) { # just check
|
|
die "version $version insufficient for $mode/$size/$level"
|
|
if $version < $minv;
|
|
}
|
|
else {
|
|
my $req = $args{min_version} // 1;
|
|
$version = $minv < $req ? $req : $minv;
|
|
}
|
|
}
|
|
elsif (defined($version)) {
|
|
my $min_rank = $rank_for->{$min_level};
|
|
for my $candidate (qw< H Q M L >) {
|
|
last if $rank_for->{$candidate} < $min_rank;
|
|
my $minv = qrspec_min_version_for($mode, $size, $candidate);
|
|
if ($minv <= $version) {
|
|
$level = $candidate;
|
|
last;
|
|
}
|
|
}
|
|
die "no level for $mode/$size/$version (min rank: $min_rank)"
|
|
unless defined $level;
|
|
}
|
|
else { # nothing is defined, go for the smaller size
|
|
$level = $min_level;
|
|
my $min_rank = $rank_for->{$level};
|
|
my $minv = qrspec_min_version_for($mode, $size, $level);
|
|
my $req = $args{'min-version'} // 1;
|
|
$version = $minv <= $req ? $req : $minv;
|
|
for my $candidate (qw< L M Q H >) {
|
|
next if $rank_for->{$candidate} <= $min_rank;
|
|
my $altv = qrspec_min_version_for($mode, $size, $candidate);
|
|
last if $altv > $minv;
|
|
$level = $candidate;
|
|
}
|
|
}
|
|
|
|
return (
|
|
%args,
|
|
mode => $mode,
|
|
level => $level,
|
|
version => $version,
|
|
);
|
|
}
|
|
|
|
sub qr_encode (@args) {
|
|
my %args = qr_best_params(@args);
|
|
my $mode = $args{mode};
|
|
my $level = $args{level};
|
|
my $size = length($args{octets});
|
|
$args{version} //= qrspec_min_version_for($mode, $size, $level);
|
|
_add_encoded(\%args);
|
|
_add_codewords(\%args);
|
|
_add_error_correction(\%args);
|
|
add_matrix(\%args);
|
|
_add_plot(\%args);
|
|
return \%args;
|
|
}
|
|
|
|
sub _add_plot ($args) {
|
|
$args->{plot} = [
|
|
map { [ map { $_ & 0x01 ? '*' : ' ' } $_->@* ] } $args->{matrix}->@*
|
|
];
|
|
return $args;
|
|
}
|
|
|
|
sub _add_encoded ($args) {
|
|
state $encoder_for = {
|
|
numeric => \&_qr_encode_numeric,
|
|
alphanumeric => \&_qr_encode_alphanumeric,
|
|
byte => \&_qr_encode_byte,
|
|
kanji => \&_qr_encode_kanji,
|
|
};
|
|
|
|
my $mode = $args->{mode};
|
|
my $encoder = $encoder_for->{$mode} or die "missing mode <$mode>\n";
|
|
my $mi = qrspec_mode_indicator($mode);
|
|
|
|
my $version = $args->{version};
|
|
my $size = length($args->{octets});
|
|
|
|
my $lis = qrspec_length_indicator($mode, $args->{version});
|
|
my $li = _dec2bin(length($args->{octets}), $lis);
|
|
|
|
$args->{encoded} = $mi . $li . $encoder->($args->{octets});
|
|
|
|
return $args;
|
|
}
|
|
|
|
sub _add_codewords ($args) {
|
|
my $bit_stream = $args->{encoded};
|
|
my $data_size = qrspec_data_size($args->@{qw< version level >});
|
|
my $needed_bits = length($bit_stream);
|
|
my $residual_bits = 8 * $data_size - $needed_bits;
|
|
die "not enough bits, wrong version?\n" if $residual_bits < 0;
|
|
my $terminator_size = $residual_bits >= 4 ? 4 : $residual_bits;
|
|
$bit_stream .= '0' x $terminator_size;
|
|
$residual_bits -= $terminator_size;
|
|
if (my $pad1 = $residual_bits % 8) {
|
|
$bit_stream .= '0' x $pad1;
|
|
$residual_bits -= $pad1;
|
|
}
|
|
while ($residual_bits > 0) {
|
|
$bit_stream .= '11101100';
|
|
last if $residual_bits == 8;
|
|
$bit_stream .= '00010001';
|
|
$residual_bits -= 16;
|
|
}
|
|
$args->{bit_stream} = $bit_stream;
|
|
$args->{codewords} = pack 'B*', $bit_stream;
|
|
return $args;
|
|
}
|
|
|
|
sub _add_error_correction ($args) {
|
|
my @blocks = qrspec_ecc_spec($args->@{qw< version level >});
|
|
$args->{ecc} = \@blocks;
|
|
my $expanded = '';
|
|
my $codewords = $args->{codewords};
|
|
my $i = 0;
|
|
my (@codewords, @eccs);
|
|
for my $block (@blocks) {
|
|
my ($ecc, $data, $count) = $block->@{qw< ecc data count >};
|
|
while ($count-- > 0) {
|
|
my $cw = substr($codewords, $i, $data);
|
|
push @codewords, $cw;
|
|
push @eccs, rs_correction_string($cw, $ecc);
|
|
$i += $data;
|
|
}
|
|
}
|
|
$args->{expanded} = _linearize(\@codewords) . _linearize(\@eccs);
|
|
$args->{remainder} = qrspec_remainder($args->{version});
|
|
return $args;
|
|
}
|
|
|
|
sub _linearize ($strings) {
|
|
return $strings->[0] if $strings->@* == 1;
|
|
my $retval = '';
|
|
my $i = 0;
|
|
my $n = length($strings->[-1]);
|
|
while ($i < $n) {
|
|
for my $string ($strings->@*) {
|
|
next if $i >= length($string);
|
|
$retval .= substr($string, $i, 1);
|
|
}
|
|
++$i;
|
|
}
|
|
return $retval;
|
|
}
|
|
|
|
sub _dec2bin ($v, $n) { substr(unpack('B*', pack('N', $v)), -$n, $n) }
|
|
|
|
sub _qr_encode_numeric ($octets) {
|
|
state $n_bits_for = [ 4, 7, 10 ];
|
|
my $i = 0; # index of start of substr, advanced each iteration
|
|
my $r = length($octets); # number of residual octets to take
|
|
my $bits = '';
|
|
while ($r > 0) {
|
|
my $l = $r >= 3 ? 3 : $r;
|
|
$bits .= _dec2bin(substr($octets, $i, $l), $n_bits_for->[$l - 1]);
|
|
$r -= $l;
|
|
$i += $l;
|
|
}
|
|
return $bits;
|
|
}
|
|
|
|
sub _qr_encode_alphanumeric ($octets) {
|
|
state $chars = [ 0 .. 9, 'A' .. 'Z', split //, ' $%*+-./:' ];
|
|
state $value_for = { map { $chars->[$_] => $_ } 0 .. $chars->$#* };
|
|
my $i = 0; # index of start of substr, advanced each iteration
|
|
my $r = length($octets); # number of residual octets to take
|
|
my $bits = '';
|
|
while ($r > 0) {
|
|
if ($r == 1) {
|
|
$bits .= _dec2bin($value_for->{substr($octets, $i, 1)}, 6);
|
|
$r = 0;
|
|
}
|
|
else {
|
|
my $value = $value_for->{substr($octets, $i++, 1)} * 45;
|
|
$value += $value_for->{substr($octets, $i++, 1)};
|
|
$bits .= _dec2bin($value, 11);
|
|
$r -= 2;
|
|
}
|
|
}
|
|
return $bits;
|
|
}
|
|
|
|
sub _qr_encode_kanji ($octets) {
|
|
my $i = 0;
|
|
my $r = length($octets);
|
|
my $bits = '';
|
|
while ($r > 0) {
|
|
my $v = unpack('n', substr($octets, $i, 2));
|
|
$v -= ($v <= 0x9FFC) ? 0x8140 : 0xC140;
|
|
$v = ($v >> 8) * 0xC0 + ($v & 0xFF);
|
|
$bits .= _dec2bin($v, 13);
|
|
$r -= 2;
|
|
$i += 2;
|
|
}
|
|
return $bits;
|
|
}
|
|
|
|
sub _qr_encode_byte ($octets) { unpack 'B*', $octets }
|
|
|
|
|
|
1;
|