| 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 |