line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Data::Password::zxcvbn::Match::Spatial; |
2
|
3
|
|
|
3
|
|
1463
|
use Moo; |
|
3
|
|
|
|
|
9
|
|
|
3
|
|
|
|
|
18
|
|
3
|
|
|
|
|
|
|
with 'Data::Password::zxcvbn::Match'; |
4
|
3
|
|
|
3
|
|
3749
|
use Data::Password::zxcvbn::Combinatorics qw(nCk); |
|
3
|
|
|
|
|
8
|
|
|
3
|
|
|
|
|
222
|
|
5
|
3
|
|
|
3
|
|
36
|
use List::AllUtils qw(min); |
|
3
|
|
|
|
|
10
|
|
|
3
|
|
|
|
|
2547
|
|
6
|
|
|
|
|
|
|
our $VERSION = '1.1.2'; # VERSION |
7
|
|
|
|
|
|
|
# ABSTRACT: match class for sequences of nearby keys |
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
|
10
|
|
|
|
|
|
|
# this should be constrained to the keys of %graphs, but we can't do |
11
|
|
|
|
|
|
|
# that because users can pass their own graphs to ->make |
12
|
|
|
|
|
|
|
has graph_name => (is=>'ro',default=>'qwerty'); |
13
|
|
|
|
|
|
|
has graph_meta => (is=>'ro',default=>sub {+{}}); |
14
|
|
|
|
|
|
|
has shifted_count => (is=>'ro',default=>0); |
15
|
|
|
|
|
|
|
has turns => (is=>'ro',default=>1); |
16
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
sub estimate_guesses { |
19
|
642
|
|
|
642
|
1
|
15505
|
my ($self,$min_guesses) = @_; |
20
|
|
|
|
|
|
|
|
21
|
642
|
|
|
|
|
2353
|
my $starts = $self->graph_meta->{starting_positions}; |
22
|
642
|
|
|
|
|
1553
|
my $degree = $self->graph_meta->{average_degree}; |
23
|
|
|
|
|
|
|
|
24
|
642
|
|
|
|
|
1344
|
my $guesses = 0; |
25
|
642
|
|
|
|
|
1410
|
my $length = length($self->token); |
26
|
642
|
|
|
|
|
1668
|
my $turns = $self->turns; |
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
# estimate the number of possible patterns w/ length $length or |
29
|
|
|
|
|
|
|
# less with $turns turns or less. |
30
|
642
|
|
|
|
|
1888
|
for my $i (2..$length) { |
31
|
1528
|
|
|
|
|
4211
|
my $possible_turns = min($turns, $i-1); |
32
|
1528
|
|
|
|
|
3145
|
for my $j (1..$possible_turns) { |
33
|
2453
|
|
|
|
|
6463
|
$guesses += nCk($i-1,$j-1) * $starts * $degree**$j; |
34
|
|
|
|
|
|
|
} |
35
|
|
|
|
|
|
|
} |
36
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
# add extra guesses for shifted keys. (% instead of 5, A instead |
38
|
|
|
|
|
|
|
# of a.) math is similar to extra guesses of l33t substitutions |
39
|
|
|
|
|
|
|
# in dictionary matches. |
40
|
|
|
|
|
|
|
|
41
|
642
|
100
|
|
|
|
2886
|
if (my $shifts = $self->shifted_count) { |
42
|
20
|
|
|
|
|
64
|
my $unshifts = $length - $shifts; |
43
|
20
|
100
|
66
|
|
|
141
|
if ($shifts == 0 || $unshifts == 0) { |
44
|
5
|
|
|
|
|
20
|
$guesses *= 2; |
45
|
|
|
|
|
|
|
} |
46
|
|
|
|
|
|
|
else { |
47
|
15
|
|
|
|
|
39
|
my $shifted_variations = 0; |
48
|
15
|
|
|
|
|
68
|
for my $i (1..min($shifts,$unshifts)) { |
49
|
16
|
|
|
|
|
57
|
$shifted_variations += nCk($length,$i); |
50
|
|
|
|
|
|
|
} |
51
|
15
|
|
|
|
|
44
|
$guesses *= $shifted_variations; |
52
|
|
|
|
|
|
|
} |
53
|
|
|
|
|
|
|
} |
54
|
|
|
|
|
|
|
|
55
|
642
|
|
|
|
|
3105
|
return $guesses; |
56
|
|
|
|
|
|
|
} |
57
|
|
|
|
|
|
|
|
58
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
sub make { |
60
|
1511
|
|
|
1511
|
1
|
97824
|
my ($class, $password, $opts) = @_; |
61
|
|
|
|
|
|
|
my $graphs = $opts->{graphs} |
62
|
1511
|
|
66
|
|
|
7501
|
|| do { |
63
|
|
|
|
|
|
|
require Data::Password::zxcvbn::AdjacencyGraph; |
64
|
|
|
|
|
|
|
\%Data::Password::zxcvbn::AdjacencyGraph::graphs; ## no critic (ProhibitPackageVars) |
65
|
|
|
|
|
|
|
}; |
66
|
|
|
|
|
|
|
|
67
|
1511
|
|
|
|
|
4207
|
my $length = length($password); |
68
|
1511
|
|
|
|
|
3646
|
my @matches = (); |
69
|
1511
|
|
|
|
|
2980
|
for my $name (keys %{$graphs}) { |
|
1511
|
|
|
|
|
7577
|
|
70
|
5999
|
|
|
|
|
17730
|
my $graph = $graphs->{$name}{keys}; |
71
|
|
|
|
|
|
|
|
72
|
5999
|
|
|
|
|
9375
|
my $i=0; |
73
|
5999
|
|
|
|
|
14524
|
while ($i < $length-1) { |
74
|
23562
|
|
|
|
|
37362
|
my $j = $i+1; |
75
|
|
|
|
|
|
|
# this has to be different from the -1 used later, and |
76
|
|
|
|
|
|
|
# different from the direction indices (usually 0..3) |
77
|
23562
|
|
|
|
|
34599
|
my $last_direction = -2; |
78
|
23562
|
|
|
|
|
32381
|
my $turns = 0; |
79
|
23562
|
100
|
100
|
|
|
83796
|
my $shifted_count = ( |
80
|
|
|
|
|
|
|
$name !~ m{keypad} && |
81
|
|
|
|
|
|
|
substr($password,$i,1) =~ |
82
|
|
|
|
|
|
|
m{[~!@#\$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?]} |
83
|
|
|
|
|
|
|
) |
84
|
|
|
|
|
|
|
? 1 # first character is shifted |
85
|
|
|
|
|
|
|
: 0; |
86
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
GROW: |
88
|
23562
|
|
|
|
|
34223
|
while (1) { |
89
|
27084
|
|
|
|
|
37162
|
my $found = 0; |
90
|
|
|
|
|
|
|
# consider growing pattern by one character if j |
91
|
|
|
|
|
|
|
# hasn't gone over the edge. |
92
|
27084
|
100
|
|
|
|
51583
|
if ($j < $length) { |
93
|
26324
|
|
|
|
|
35428
|
my $found_direction = -1; my $cur_direction = -1; |
|
26324
|
|
|
|
|
35226
|
|
94
|
26324
|
|
|
|
|
44839
|
my $prev_character = substr($password,$j-1,1); |
95
|
26324
|
|
|
|
|
43228
|
my $cur_character = substr($password,$j,1); |
96
|
|
|
|
|
|
|
ADJACENCY: |
97
|
26324
|
100
|
|
|
|
35276
|
for my $adj (@{ $graph->{$prev_character} || [] }) { |
|
26324
|
|
|
|
|
111215
|
|
98
|
|
|
|
|
|
|
## no critic (ProhibitDeepNests) |
99
|
96859
|
|
|
|
|
128101
|
++$cur_direction; |
100
|
96859
|
100
|
100
|
|
|
290075
|
if (defined($adj) && |
101
|
|
|
|
|
|
|
(my $idx = index($adj,$cur_character)) >= 0) { |
102
|
3522
|
|
|
|
|
5638
|
$found=1; $found_direction = $cur_direction; |
|
3522
|
|
|
|
|
5697
|
|
103
|
|
|
|
|
|
|
# index 1 in the adjacency means the key |
104
|
|
|
|
|
|
|
# is shifted, 0 means unshifted: A vs a, % |
105
|
|
|
|
|
|
|
# vs 5, etc. for example, 'q' is adjacent |
106
|
|
|
|
|
|
|
# to the entry '2@'. @ is shifted w/ |
107
|
|
|
|
|
|
|
# index 1, 2 is unshifted. |
108
|
3522
|
100
|
|
|
|
7571
|
++$shifted_count if $idx==1; |
109
|
3522
|
100
|
|
|
|
8167
|
if ($last_direction != $cur_direction) { |
110
|
|
|
|
|
|
|
# adding a turn is correct even in the |
111
|
|
|
|
|
|
|
# initial case when last_direction is |
112
|
|
|
|
|
|
|
# -2: every spatial pattern starts |
113
|
|
|
|
|
|
|
# with a turn. |
114
|
3208
|
|
|
|
|
4970
|
++$turns; |
115
|
3208
|
|
|
|
|
4612
|
$last_direction = $cur_direction; |
116
|
|
|
|
|
|
|
} |
117
|
|
|
|
|
|
|
# found a match, stop looking at this key |
118
|
3522
|
|
|
|
|
7081
|
last ADJACENCY; |
119
|
|
|
|
|
|
|
} |
120
|
|
|
|
|
|
|
} |
121
|
|
|
|
|
|
|
} |
122
|
|
|
|
|
|
|
|
123
|
27084
|
100
|
|
|
|
51258
|
if ($found) { |
124
|
|
|
|
|
|
|
# if the current pattern continued, extend j and |
125
|
|
|
|
|
|
|
# try to grow again |
126
|
3522
|
|
|
|
|
5650
|
++$j; |
127
|
|
|
|
|
|
|
} |
128
|
|
|
|
|
|
|
else { |
129
|
|
|
|
|
|
|
# otherwise push the pattern discovered so far, if |
130
|
|
|
|
|
|
|
# any... |
131
|
23562
|
|
|
|
|
33091
|
my %meta = %{ $graphs->{$name} }; |
|
23562
|
|
|
|
|
71040
|
|
132
|
23562
|
|
|
|
|
42220
|
delete $meta{keys}; |
133
|
23562
|
100
|
|
|
|
68264
|
push @matches, $class->new({ |
134
|
|
|
|
|
|
|
i => $i, j => $j-1, |
135
|
|
|
|
|
|
|
token => substr($password,$i,$j-$i), |
136
|
|
|
|
|
|
|
graph_name => $name, |
137
|
|
|
|
|
|
|
graph_meta => \%meta, |
138
|
|
|
|
|
|
|
turns => $turns, |
139
|
|
|
|
|
|
|
shifted_count => $shifted_count, |
140
|
|
|
|
|
|
|
}) unless $j-$i<=2; # don't consider short chains |
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
# ...and then start a new search for the rest of |
143
|
|
|
|
|
|
|
# the password. |
144
|
23562
|
|
|
|
|
62240
|
$i = $j; |
145
|
23562
|
|
|
|
|
61805
|
last GROW; |
146
|
|
|
|
|
|
|
} |
147
|
|
|
|
|
|
|
} |
148
|
|
|
|
|
|
|
} |
149
|
|
|
|
|
|
|
} |
150
|
|
|
|
|
|
|
|
151
|
1511
|
|
|
|
|
6928
|
@matches = sort @matches; |
152
|
1511
|
|
|
|
|
6200
|
return \@matches; |
153
|
|
|
|
|
|
|
} |
154
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
|
156
|
|
|
|
|
|
|
sub feedback_warning { |
157
|
6
|
|
|
6
|
1
|
20
|
my ($self) = @_; |
158
|
|
|
|
|
|
|
|
159
|
6
|
100
|
|
|
|
54
|
return $self->turns == 1 |
160
|
|
|
|
|
|
|
? 'Straight rows of keys are easy to guess' |
161
|
|
|
|
|
|
|
: 'Short keyboard patterns are easy to guess' |
162
|
|
|
|
|
|
|
; |
163
|
|
|
|
|
|
|
} |
164
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
sub feedback_suggestions { |
166
|
6
|
|
|
6
|
1
|
34
|
return [ 'Use a longer keyboard pattern with more turns' ]; |
167
|
|
|
|
|
|
|
} |
168
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
|
170
|
|
|
|
|
|
|
around fields_for_json => sub { |
171
|
|
|
|
|
|
|
my ($orig,$self) = @_; |
172
|
|
|
|
|
|
|
( $self->$orig(), qw(graph_name shifted_count turns) ) |
173
|
|
|
|
|
|
|
}; |
174
|
|
|
|
|
|
|
|
175
|
|
|
|
|
|
|
1; |
176
|
|
|
|
|
|
|
|
177
|
|
|
|
|
|
|
__END__ |
178
|
|
|
|
|
|
|
|
179
|
|
|
|
|
|
|
=pod |
180
|
|
|
|
|
|
|
|
181
|
|
|
|
|
|
|
=encoding UTF-8 |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
=head1 NAME |
184
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
Data::Password::zxcvbn::Match::Spatial - match class for sequences of nearby keys |
186
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
=head1 VERSION |
188
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
version 1.1.2 |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
=head1 DESCRIPTION |
192
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
This class represents the guess that a certain substring of a password |
194
|
|
|
|
|
|
|
can be obtained by moving a finger in a continuous line on a keyboard. |
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
=head1 ATTRIBUTES |
197
|
|
|
|
|
|
|
|
198
|
|
|
|
|
|
|
=head2 C<graph_name> |
199
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
The name of the keyboard / adjacency graph used for this match |
201
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
=head2 C<graph_meta> |
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
Hashref, spatial information about the graph: |
205
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
=over 4 |
207
|
|
|
|
|
|
|
|
208
|
|
|
|
|
|
|
=item * |
209
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
C<starting_positions> |
211
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
the number of keys in the keyboard, or starting nodes in the graph |
213
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
=item * |
215
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
C<average_degree> |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
the average number of neighbouring keys, or average out-degree of the graph |
219
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
=back |
221
|
|
|
|
|
|
|
|
222
|
|
|
|
|
|
|
=head2 C<shifted_count> |
223
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
How many of the keys need to be "shifted" to produce the token |
225
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
=head2 C<turns> |
227
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
How many times the finger must have changed direction to produce the |
229
|
|
|
|
|
|
|
token |
230
|
|
|
|
|
|
|
|
231
|
|
|
|
|
|
|
=head1 METHODS |
232
|
|
|
|
|
|
|
|
233
|
|
|
|
|
|
|
=head2 C<estimate_guesses> |
234
|
|
|
|
|
|
|
|
235
|
|
|
|
|
|
|
The number of guesses grows super-linearly with the length of the |
236
|
|
|
|
|
|
|
pattern, the number of L</turns>, and the amount of L<shifted |
237
|
|
|
|
|
|
|
keys|/shifted_count>. |
238
|
|
|
|
|
|
|
|
239
|
|
|
|
|
|
|
=head2 C<make> |
240
|
|
|
|
|
|
|
|
241
|
|
|
|
|
|
|
my @matches = @{ Data::Password::zxcvbn::Match::Spatial->make( |
242
|
|
|
|
|
|
|
$password, |
243
|
|
|
|
|
|
|
{ # this is the default |
244
|
|
|
|
|
|
|
graphs => \%Data::Password::zxcvbn::AdjacencyGraph::graphs, |
245
|
|
|
|
|
|
|
}, |
246
|
|
|
|
|
|
|
) }; |
247
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
Scans the C<$password> for substrings that can be produced by typing |
249
|
|
|
|
|
|
|
on the keyboards described by the C<graphs>. |
250
|
|
|
|
|
|
|
|
251
|
|
|
|
|
|
|
The data structure needed for C<graphs> is a bit complicated; look at |
252
|
|
|
|
|
|
|
the L<< C<build-keyboard-adjacency-graphs> script in the |
253
|
|
|
|
|
|
|
distribution's |
254
|
|
|
|
|
|
|
repository|https://bitbucket.org/broadbean/p5-data-password-zxcvbn/src/master/maint/build-keyboard-adjacency-graphs |
255
|
|
|
|
|
|
|
>>. |
256
|
|
|
|
|
|
|
|
257
|
|
|
|
|
|
|
=head2 C<feedback_warning> |
258
|
|
|
|
|
|
|
|
259
|
|
|
|
|
|
|
=head2 C<feedback_suggestions> |
260
|
|
|
|
|
|
|
|
261
|
|
|
|
|
|
|
This class suggests that short keyboard patterns are easy to guess, |
262
|
|
|
|
|
|
|
and to use longer and less straight ones. |
263
|
|
|
|
|
|
|
|
264
|
|
|
|
|
|
|
=head2 C<fields_for_json> |
265
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
The JSON serialisation for matches of this class will contain C<token |
267
|
|
|
|
|
|
|
i j guesses guesses_log10 graph_name shifted_count turns>. |
268
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
=head1 AUTHOR |
270
|
|
|
|
|
|
|
|
271
|
|
|
|
|
|
|
Gianni Ceccarelli <gianni.ceccarelli@broadbean.com> |
272
|
|
|
|
|
|
|
|
273
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
274
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
This software is copyright (c) 2022 by BroadBean UK, a CareerBuilder Company. |
276
|
|
|
|
|
|
|
|
277
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
278
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
279
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
=cut |