File Coverage

blib/lib/Data/Password/zxcvbn/Match/Spatial.pm
Criterion Covered Total %
statement 72 72 100.0
branch 22 22 100.0
condition 10 12 83.3
subroutine 7 7 100.0
pod 4 4 100.0
total 115 117 98.2


line stmt bran cond sub pod time code
1             package Data::Password::zxcvbn::Match::Spatial;
2 3     3   1532 use Moo;
  3         7  
  3         19  
3             with 'Data::Password::zxcvbn::Match';
4 3     3   3755 use Data::Password::zxcvbn::Combinatorics qw(nCk);
  3         8  
  3         197  
5 3     3   19 use List::AllUtils qw(min);
  3         20  
  3         2520  
6             our $VERSION = '1.1.0'; # 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 15495 my ($self,$min_guesses) = @_;
20              
21 642         2420 my $starts = $self->graph_meta->{starting_positions};
22 642         1566 my $degree = $self->graph_meta->{average_degree};
23              
24 642         1226 my $guesses = 0;
25 642         1638 my $length = length($self->token);
26 642         1665 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         1787 for my $i (2..$length) {
31 1528         4094 my $possible_turns = min($turns, $i-1);
32 1528         3056 for my $j (1..$possible_turns) {
33 2453         6172 $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       2791 if (my $shifts = $self->shifted_count) {
42 20         53 my $unshifts = $length - $shifts;
43 20 100 66     140 if ($shifts == 0 || $unshifts == 0) {
44 5         16 $guesses *= 2;
45             }
46             else {
47 15         40 my $shifted_variations = 0;
48 15         78 for my $i (1..min($shifts,$unshifts)) {
49 16         58 $shifted_variations += nCk($length,$i);
50             }
51 15         48 $guesses *= $shifted_variations;
52             }
53             }
54              
55 642         3063 return $guesses;
56             }
57              
58              
59             sub make {
60 1511     1511 1 99528 my ($class, $password, $opts) = @_;
61             my $graphs = $opts->{graphs}
62 1511   66     7622 || do {
63             require Data::Password::zxcvbn::AdjacencyGraph;
64             \%Data::Password::zxcvbn::AdjacencyGraph::graphs; ## no critic (ProhibitPackageVars)
65             };
66              
67 1511         4271 my $length = length($password);
68 1511         4260 my @matches = ();
69 1511         3316 for my $name (keys %{$graphs}) {
  1511         7573  
70 5999         17678 my $graph = $graphs->{$name}{keys};
71              
72 5999         10105 my $i=0;
73 5999         14906 while ($i < $length-1) {
74 23562         36437 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         34802 my $last_direction = -2;
78 23562         33542 my $turns = 0;
79 23562 100 100     84278 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         35528 while (1) {
89 27084         40222 my $found = 0;
90             # consider growing pattern by one character if j
91             # hasn't gone over the edge.
92 27084 100       50080 if ($j < $length) {
93 26324         37058 my $found_direction = -1; my $cur_direction = -1;
  26324         35299  
94 26324         46395 my $prev_character = substr($password,$j-1,1);
95 26324         39343 my $cur_character = substr($password,$j,1);
96             ADJACENCY:
97 26324 100       37008 for my $adj (@{ $graph->{$prev_character} || [] }) {
  26324         97147  
98             ## no critic (ProhibitDeepNests)
99 96859         130427 ++$cur_direction;
100 96859 100 100     295644 if (defined($adj) &&
101             (my $idx = index($adj,$cur_character)) >= 0) {
102 3522         6180 $found=1; $found_direction = $cur_direction;
  3522         6071  
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       7828 ++$shifted_count if $idx==1;
109 3522 100       8656 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         4912 ++$turns;
115 3208         5040 $last_direction = $cur_direction;
116             }
117             # found a match, stop looking at this key
118 3522         6602 last ADJACENCY;
119             }
120             }
121             }
122              
123 27084 100       53090 if ($found) {
124             # if the current pattern continued, extend j and
125             # try to grow again
126 3522         5691 ++$j;
127             }
128             else {
129             # otherwise push the pattern discovered so far, if
130             # any...
131 23562         35187 my %meta = %{ $graphs->{$name} };
  23562         66464  
132 23562         43348 delete $meta{keys};
133 23562 100       66128 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         62146 $i = $j;
145 23562         63047 last GROW;
146             }
147             }
148             }
149             }
150              
151 1511         4717 @matches = sort @matches;
152 1511         6590 return \@matches;
153             }
154              
155              
156             sub feedback_warning {
157 6     6 1 35 my ($self) = @_;
158              
159 6 100       56 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 32 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.0
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