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]
395 lines
11 KiB
Perl
395 lines
11 KiB
Perl
package QRCode::Encoder::Matrix;
|
|
use v5.24;
|
|
use experimental qw< signatures >;
|
|
use List::Util qw< sum >;
|
|
use QRCode::Encoder::QRSpec qw<
|
|
qrspec_version_pattern
|
|
qrspec_format_pattern
|
|
qrspec_alignment_patterns
|
|
>;
|
|
|
|
use Exporter qw< import >;
|
|
our @EXPORT_OK = qw< add_matrix >;
|
|
|
|
# Parts liberally taken from libqrencode/qrspec.c, which is distributed
|
|
# with LGPL license
|
|
|
|
sub add_matrix ($data) {
|
|
add_base_matrix($data);
|
|
add_quiet($data);
|
|
add_finders($data);
|
|
add_format_reservations($data); # MUST: before add_timing
|
|
add_version($data);
|
|
add_timing($data);
|
|
add_alignments($data);
|
|
add_codewords($data);
|
|
add_mask($data);
|
|
return $data;
|
|
}
|
|
|
|
sub stringify_matrix ($data) {
|
|
my @chunks;
|
|
my $matrix = $data->{matrix};
|
|
for my $row ($matrix->@*) {
|
|
push @chunks, join '', map { chr($_) } $row->@*;
|
|
}
|
|
return join "\n", @chunks;
|
|
}
|
|
|
|
sub stringify_matrix_2 ($data) {
|
|
my @chunks;
|
|
my $matrix = $data->{matrix};
|
|
for my $row ($matrix->@*) {
|
|
push @chunks, join '', map { ($_ & 0x1) ? '#' : ' ' } $row->@*;
|
|
}
|
|
return join "\n", @chunks;
|
|
}
|
|
|
|
sub add_base_matrix ($data) {
|
|
my $side = $data->{side_size} = 17 + 4 * $data->{version};
|
|
my $eside = $data->{eside_size} = $side + 8;
|
|
$data->{matrix} = [ map { [ ( 0x38 ) x $eside ] } 1 .. $eside ];
|
|
return $data;
|
|
}
|
|
|
|
sub add_finders ($data) {
|
|
my $eside_size = $data->{eside_size};
|
|
add_finder($data, 4 - 1, 4 - 1);
|
|
add_finder($data, 4 - 1, $eside_size - 8 - 4);
|
|
add_finder($data, $eside_size - 8 - 4, 4 - 1);
|
|
return $data;
|
|
}
|
|
|
|
sub add_quiet ($data) {
|
|
my $es = $data->{eside_size};
|
|
my $matrix = $data->{matrix};
|
|
for my $i (0 .. 3) {
|
|
for my $j (0 .. $es - 1) {
|
|
$matrix->[$i][$j] =
|
|
$matrix->[$es - 1 - $i][$j] =
|
|
$matrix->[$j][$i] =
|
|
$matrix->[$j][$es - 1 - $i] = 0x30;
|
|
}
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
sub add_finder ($data, $x, $y) {
|
|
state $shape = [
|
|
[ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30 ],
|
|
[ 0x30, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x30 ],
|
|
[ 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, 0x30 ],
|
|
[ 0x30, 0x31, 0x30, 0x31, 0x31, 0x31, 0x30, 0x31, 0x30 ],
|
|
[ 0x30, 0x31, 0x30, 0x31, 0x31, 0x31, 0x30, 0x31, 0x30 ],
|
|
[ 0x30, 0x31, 0x30, 0x31, 0x31, 0x31, 0x30, 0x31, 0x30 ],
|
|
[ 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, 0x30 ],
|
|
[ 0x30, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x30 ],
|
|
[ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30 ],
|
|
];
|
|
my $matrix = $data->{matrix};
|
|
for my $yoff (0 .. 8) {
|
|
my $Y = $y + $yoff;
|
|
for my $xoff (0 .. 8) {
|
|
my $X = $x + $xoff;
|
|
$matrix->[$Y][$X] = $shape->[$yoff][$xoff];
|
|
}
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
sub add_format_reservations ($data) {
|
|
my $matrix = $data->{matrix};
|
|
my $es = $data->{eside_size};
|
|
for my $i (0 .. 7) {
|
|
$matrix->[12][$i + 4] =
|
|
$matrix->[12][$i + $es - 4 - 8] =
|
|
$matrix->[$i + $es - 4 - 8][12] =
|
|
$matrix->[$i + 4][12] = 0x32;
|
|
}
|
|
$matrix->[12][12] = 0x32;
|
|
$matrix->[$es - 4 - 8][12] = 0x31;
|
|
return $data;
|
|
}
|
|
|
|
sub add_version_reservations ($data) {
|
|
return $data->{version} <= 6;
|
|
|
|
my $matrix = $data->{matrix};
|
|
my $ecstart = $data->{eside_size} - 4 - 7 - 4;
|
|
for my $i (4 .. 9) {
|
|
for my $j ($ecstart .. ($ecstart + 2)) {
|
|
$matrix->[$i][$j] = $matrix->[$j][$i] = 0x32;
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
sub add_timing ($data) {
|
|
my $matrix = $data->{matrix};
|
|
my $es = $data->{eside_size};
|
|
for my $i (12 .. ($es - 4 - 8 - 1)) {
|
|
$matrix->[$i][10] = $matrix->[10][$i] = 0x35 ^ ($i & 1);
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
sub try_add_alignment_pattern ($data, $x, $y) {
|
|
state $shape = [
|
|
[ 0x31, 0x31, 0x31, 0x31, 0x31 ],
|
|
[ 0x31, 0x30, 0x30, 0x30, 0x31 ],
|
|
[ 0x31, 0x30, 0x31, 0x30, 0x31 ],
|
|
[ 0x31, 0x30, 0x30, 0x30, 0x31 ],
|
|
[ 0x31, 0x31, 0x31, 0x31, 0x31 ],
|
|
];
|
|
$x += 4; # offset by quiet zone
|
|
$y += 4; # offset by quiet zone
|
|
my $matrix = $data->{matrix};
|
|
return if $matrix->[$y][$x] < 0x34
|
|
|| $matrix->[$y][$x + 4] < 0x34
|
|
|| $matrix->[$y + 4][$x] < 0x34;
|
|
for my $i (0 .. 4) {
|
|
for my $j (0 .. 4) {
|
|
$matrix->[$y + $i][$x + $j] = $shape->[$i][$j];
|
|
}
|
|
}
|
|
}
|
|
|
|
sub add_alignments ($data) {
|
|
my @offset = qrspec_alignment_patterns($data->{version});
|
|
for my $y_center (@offset) {
|
|
for my $x_center (@offset) {
|
|
try_add_alignment_pattern($data, $x_center - 2, $y_center - 2);
|
|
}
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
sub bits_iterator ($data) {
|
|
my $n_expanded = length($data->{expanded});
|
|
my $rem = $data->{remainder};
|
|
my $i = 0;
|
|
my @queue;
|
|
return sub {
|
|
if (! @queue) {
|
|
if ($i < $n_expanded) {
|
|
push @queue, split m{}mxs, unpack 'B*', substr($data->{expanded}, $i++, 1);
|
|
}
|
|
else {
|
|
push @queue, ('0') x $rem;
|
|
$rem = 0;
|
|
}
|
|
}
|
|
return shift(@queue);
|
|
};
|
|
}
|
|
|
|
sub add_codewords ($data) {
|
|
my $it = bits_iterator($data);
|
|
my $matrix = $data->{matrix};
|
|
my $side_size = $data->{side_size};
|
|
|
|
# start from a fake position that would be the last bit of a
|
|
# hypothetical "-1" codeword
|
|
my $x = $side_size - 2;
|
|
my $y = $side_size;
|
|
my $left = 1;
|
|
my $d = -1; # direction
|
|
while (defined(my $bit = $it->())) {
|
|
while ('necessary') {
|
|
if ($x % 2 == $left) {
|
|
++$x;
|
|
$y += $d;
|
|
}
|
|
else {
|
|
--$x;
|
|
}
|
|
if ($d < 0 && $y < 0) { # reset condition
|
|
$x -= 2;
|
|
$y = 0;
|
|
$d = 1;
|
|
}
|
|
elsif ($d > 0 && $y >= $side_size) { # other reset condition
|
|
$x -= 2;
|
|
$y = $side_size - 1;
|
|
$d = -1;
|
|
}
|
|
if ($x == 6) { # left timing column, skip a column entirely
|
|
$x = 5;
|
|
$left = 0;
|
|
}
|
|
last if $matrix->[$y + 4][$x + 4] > 0x37; # found suitable position
|
|
}
|
|
$matrix->[$y + 4][$x + 4] = $bit ? 0x37 : 0x36;
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
sub evaluate_matrix ($matrix) {
|
|
return 0
|
|
+ evaluate_matrix_adjacents_and_11311($matrix)
|
|
+ evaluate_matrix_blocks($matrix)
|
|
+ evaluate_matrix_proportion($matrix);
|
|
}
|
|
|
|
sub __row ($matrix, $i) {
|
|
my $max_idx = $matrix->[0]->$#* - 4;
|
|
join('', map { $matrix->[$i + 4][$_] & 0x01 ? 1 : 0 } 4 .. $max_idx);
|
|
}
|
|
|
|
sub __col ($matrix, $i) {
|
|
my $max_idx = $matrix->[0]->$#* - 4;
|
|
join('', map { $matrix->[$_][$i + 4] & 0x01 ? 1 : 0 } 4 .. $max_idx);
|
|
}
|
|
|
|
sub evaluate_matrix_adjacents_and_11311 ($matrix) {
|
|
my $side_size = $matrix->[0]->@* - 8;
|
|
my $penalty = 0;
|
|
my $penalty2 = 0;
|
|
for my $i (0 .. ($side_size - 1)) {
|
|
for my $seq (__row($matrix, $i), __col($matrix, $i)) {
|
|
|
|
# adjacences
|
|
my @contributions =
|
|
map { $_ - 2 }
|
|
grep { $_ >= 5 }
|
|
map { length }
|
|
split m{(0+)}mxs, $seq;
|
|
$penalty += sum(@contributions) if @contributions;
|
|
|
|
# 000011311 | 113110000
|
|
my @matches = $seq =~ m{
|
|
(
|
|
(?: (?<=0000) 1011101 ) # look behind...
|
|
| (?: 1011101 (?=0000) ) # or look ahead
|
|
)
|
|
}gmxs;
|
|
$penalty2 += 40 * scalar(@matches);
|
|
|
|
}
|
|
}
|
|
return $penalty + $penalty2;
|
|
}
|
|
|
|
sub evaluate_matrix_blocks ($matrix) {
|
|
my $side_size = $matrix->[0]->@* - 8;
|
|
my $penalty = 0;
|
|
for my $i (0 .. ($side_size - 2)) {
|
|
for my $j (0 .. ($side_size - 2)) {
|
|
my $count = 0;
|
|
for my $offset ([0, 0], [0, 1], [1, 0], [1, 1]) {
|
|
my ($oi, $oj) = $offset->@*;
|
|
$count++ if $matrix->[$i + $oi + 4][$j + $oj + 4] & 1;
|
|
}
|
|
$penalty += 3 if ($count == 0) || ($count == 4);
|
|
}
|
|
}
|
|
return $penalty;
|
|
}
|
|
|
|
sub evaluate_matrix_proportion ($matrix) {
|
|
my $count = sum( map { map { $_ & 0x1 ? 1 : 0 } $_->@* } $matrix->@* );
|
|
my $side_size = $matrix->[0]->@* - 8;
|
|
my $total = $side_size * $side_size;
|
|
my $percentage = 100 * $count / $total;
|
|
my $deviation = abs($percentage - 50);
|
|
my $penalty = 10 * int($deviation / 5);
|
|
return $penalty;
|
|
}
|
|
|
|
sub masked_matrix ($data, $mask_id) {
|
|
state $mask_for = {
|
|
0 => sub ($i, $j) { (($i + $j) % 2) == 0 },
|
|
1 => sub ($i, $j) { ($i % 2) == 0 },
|
|
2 => sub ($i, $j) { ($j % 3) == 0 },
|
|
3 => sub ($i, $j) { (($i + $j) % 3) == 0 },
|
|
4 => sub ($i, $j) { ((int($i / 2) + int($j / 3)) % 2) == 0 },
|
|
5 => sub ($i, $j) { ((($i * $j) % 2) + (($i * $j) % 3)) == 0 },
|
|
6 => sub ($i, $j) { (((($i * $j) % 2) + (($i * $j) % 3)) % 2) == 0 },
|
|
7 => sub ($i, $j) { (((($i + $j) % 2) + (($i * $j) % 3)) % 2) == 0 },
|
|
};
|
|
my $matrix = $data->{matrix};
|
|
my @masked;
|
|
my $eside_size = $data->{eside_size};
|
|
my $mask = $mask_for->{$mask_id};
|
|
for my $i (0 .. ($eside_size - 1)) {
|
|
for my $j (0 .. ($eside_size - 1)) {
|
|
if (($matrix->[$i][$j] >= 0x36) && $mask->($i - 4, $j - 4)) {
|
|
$masked[$i][$j] = $matrix->[$i][$j] ^ 0x01;
|
|
}
|
|
else {
|
|
$masked[$i][$j] = $matrix->[$i][$j];
|
|
}
|
|
}
|
|
}
|
|
return \@masked;
|
|
}
|
|
|
|
sub add_mask ($data) {
|
|
my ($best_mask_id, $best_matrix, $best_penalty);
|
|
$data->{masked} = \my @masked;
|
|
for my $mask_id (0 .. 7) {
|
|
my $matrix = masked_matrix($data, $mask_id);
|
|
add_format($matrix, $data->{level}, $mask_id);
|
|
push @masked, $matrix;
|
|
my $penalty = evaluate_matrix($matrix);
|
|
($best_mask_id, $best_matrix, $best_penalty) = ($mask_id, $matrix, $penalty)
|
|
if (! $best_matrix) || $penalty < $best_penalty;
|
|
}
|
|
$data->{original_matrix} = delete($data->{matrix});
|
|
$data->{matrix} = $best_matrix;
|
|
$data->{mask_id} = $best_mask_id;
|
|
return $data;
|
|
}
|
|
|
|
sub add_format ($matrix, $level, $mask_id) {
|
|
my $fmt = qrspec_format_pattern($level, $mask_id);
|
|
my $es = $matrix->[0]->@*;
|
|
|
|
# 1st copy
|
|
my $format = $fmt;
|
|
for my $i (0 .. 7) {
|
|
$matrix->[12][$es - 1 - 4 - $i] = $format & 0x01 ? 0x31 : 0x30;
|
|
$format >>= 1;
|
|
}
|
|
for my $i (8 .. 14) {
|
|
$matrix->[$es - 1 - 4 - 14 + $i][12] = $format & 0x01 ? 0x31 : 0x30;
|
|
$format >>= 1;
|
|
}
|
|
|
|
# 2nd copy
|
|
$format = $fmt;
|
|
for my $i (0 .. 5) {
|
|
$matrix->[4 + $i][12] = $format & 0x01 ? 0x31 : 0x30;
|
|
$format >>= 1;
|
|
}
|
|
for my $i (6, 7) {
|
|
$matrix->[4 + 1 + $i][12] = $format & 0x01 ? 0x31 : 0x30;
|
|
$format >>= 1;
|
|
}
|
|
# 8
|
|
$matrix->[4 + 1 + 7][11] = $format & 0x01 ? 0x31 : 0x30;
|
|
$format >>= 1;
|
|
for my $i (9 .. 14) {
|
|
$matrix->[4 + 1 + 7][9 + 9 - $i] = $format & 0x01 ? 0x31 : 0x30;
|
|
$format >>= 1;
|
|
}
|
|
|
|
return $matrix;
|
|
}
|
|
|
|
sub add_version ($data) {
|
|
my $vp = qrspec_version_pattern($data->{version}) // return;
|
|
my $matrix = $data->{matrix};
|
|
my $ecstart = $data->{eside_size} - 4 - 7 - 4;
|
|
for my $i (4 .. 9) {
|
|
for my $j ($ecstart .. ($ecstart + 2)) {
|
|
$matrix->[$i][$j] = $matrix->[$j][$i] = 0x30 ^ ($vp & 1);
|
|
$vp >>= 1;
|
|
}
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
1;
|