1package OpenXPKI::Server::API2::Plugin::Crypto::password_quality::CheckEntropyRole;
2use feature 'unicode_strings';
3
4=head1 NAME
5
6OpenXPKI::Server::API2::Plugin::Crypto::password_quality::CheckEntropyRole -
7Check password entropy
8
9=head1 CHECKS
10
11This role adds the following checks to
12L<OpenXPKI::Server::API2::Plugin::Crypto::password_quality::Validate>:
13
14C<entropy>.
15
16Enabled by default: C<entropy>.
17
18For more information about the checks see
19L<OpenXPKI::Server::API2::Plugin::Crypto::password_quality>.
20
21=cut
22
23use Moose::Role;
24
25# Core modules
26use POSIX qw(floor);
27
28# Project modules
29use OpenXPKI::Debug;
30
31
32requires 'register_check';
33requires 'password';
34requires 'enable';
35
36
37use constant {
38    OTHER => '__unlisted character class',
39    UNICODE_ASSIGNED_CHARACTERS => 143924,
40};
41
42
43has min_entropy => (
44    is => 'rw',
45    isa => 'Int',
46    lazy => 1,
47    default => sub { 60 },
48);
49
50has _unicode_blocks => (
51    is => 'ro',
52    isa => 'HashRef',
53    traits  => ['Hash'],
54    init_arg => undef,
55    lazy => 1,
56    builder => '_build_unicode_blocks',
57    handles => {
58        # Returns the unicode block range by given block name
59        unicode_block => 'get',
60        # Returns a list of all defined unicode block names
61        unicode_block_names => 'keys',
62    },
63);
64
65sub _build_unicode_blocks {
66    my $result = {};
67    # cat ~/Unicode\ Basic\ Multilingual\ Plane.txt | perl -e 'use utf8; while (<>) { chomp; next if /^(#|$)/; ($n,$r1,$r2) = m/^(.*) \(([^-]+)-([^-]+)\)$/; $n=~s/\x{0027}/\\\x{0027}/; print "[ 0x$r1 => 0x$r2 ] => \x{0027}$n\x{0027},\n";} '
68
69    # List of selected blocks (= character classes) from Unicode's Basic Multilingual Plane (plane 0).
70    # Multiple ranges may refer to a block of the same name
71    my $unicode_plane0_selected_blocks = [
72        # manually separated:
73        [ 0x0000 => 0x001F, 'Basic Latin (Control)' ],
74        [ 0x0020 => 0x002F, 'Basic Latin (Punctuation)' ],
75        [ 0x0030 => 0x0039, 'Basic Latin (Number)' ],
76        [ 0x003A => 0x0040, 'Basic Latin (Punctuation)' ],
77        [ 0x0041 => 0x005A, 'Basic Latin (Letter, uppercase)' ],
78        [ 0x005B => 0x0060, 'Basic Latin (Punctuation)' ],
79        [ 0x0061 => 0x007A, 'Basic Latin (Letter, lowercase)' ],
80        [ 0x007B => 0x007F, 'Basic Latin (Punctuation)' ],
81        # generated:
82        [ 0x0080 => 0x00FF, 'Latin-1 Supplement' ],
83        [ 0x0100 => 0x024F, 'Latin Extended-A + Latin Extended-B' ],
84        [ 0x1D00 => 0x1DBF, 'Phonetic Extensions + Phonetic Extensions Supplement' ],
85        [ 0x1E00 => 0x1EFF, 'Latin Extended Additional' ],
86        [ 0x0250 => 0x02AF, 'IPA Extensions' ],
87        [ 0x0370 => 0x03FF, 'Greek and Coptic' ],
88        [ 0x0400 => 0x052F, 'Cyrillic + Cyrillic Supplement' ],
89        [ 0x0530 => 0x058F, 'Armenian' ],
90        [ 0x0590 => 0x05FF, 'Hebrew' ],
91        [ 0x0600 => 0x06FF, 'Arabic' ],
92        [ 0x0700 => 0x074F, 'Syriac' ],
93        [ 0x0750 => 0x077F, 'Arabic Supplement' ],
94        [ 0x0780 => 0x07BF, 'Thaana' ],
95        [ 0x07C0 => 0x07FF, 'N\'Ko' ],
96        [ 0x0800 => 0x083F, 'Samaritan' ],
97        [ 0x0840 => 0x085F, 'Mandaic' ],
98        [ 0x0860 => 0x086F, 'Syriac Supplement' ],
99        [ 0x08A0 => 0x08FF, 'Arabic Extended-A' ],
100        [ 0x0900 => 0x097F, 'Devanagari' ],
101        [ 0x0980 => 0x09FF, 'Bengali' ],
102        [ 0x0A00 => 0x0A7F, 'Gurmukhi' ],
103        [ 0x0A80 => 0x0AFF, 'Gujarati' ],
104        [ 0x0B00 => 0x0B7F, 'Oriya' ],
105        [ 0x0B80 => 0x0BFF, 'Tamil' ],
106        [ 0x0C00 => 0x0C7F, 'Telugu' ],
107        [ 0x0C80 => 0x0CFF, 'Kannada' ],
108        [ 0x0D00 => 0x0D7F, 'Malayalam' ],
109        [ 0x0D80 => 0x0DFF, 'Sinhala' ],
110        [ 0x0E00 => 0x0E7F, 'Thai' ],
111        [ 0x0E80 => 0x0EFF, 'Lao' ],
112        [ 0x0F00 => 0x0FFF, 'Tibetan' ],
113        [ 0x1000 => 0x109F, 'Myanmar' ],
114        [ 0x10A0 => 0x10FF, 'Georgian' ],
115        [ 0x1100 => 0x11FF, 'Hangul Jamo' ],
116        [ 0x1200 => 0x137F, 'Ethiopic' ],
117        [ 0x1380 => 0x139F, 'Ethiopic Supplement' ],
118        [ 0x13A0 => 0x13FF, 'Cherokee' ],
119        [ 0x1400 => 0x167F, 'Unified Canadian Aboriginal Syllabics' ],
120        [ 0x1680 => 0x169F, 'Ogham' ],
121        [ 0x1700 => 0x171F, 'Tagalog' ],
122        [ 0x1720 => 0x173F, 'Hanunoo' ],
123        [ 0x1740 => 0x175F, 'Buhid' ],
124        [ 0x1760 => 0x177F, 'Tagbanwa' ],
125        [ 0x1780 => 0x17FF, 'Khmer' ],
126        [ 0x1800 => 0x18AF, 'Mongolian' ],
127        [ 0x18B0 => 0x18FF, 'Unified Canadian Aboriginal Syllabics Extended' ],
128        [ 0x1900 => 0x194F, 'Limbu' ],
129        [ 0x1950 => 0x197F, 'Tai Le' ],
130        [ 0x1980 => 0x19DF, 'New Tai Lue' ],
131        [ 0x19E0 => 0x19FF, 'Khmer Symbols' ],
132        [ 0x1A00 => 0x1A1F, 'Buginese' ],
133        [ 0x1A20 => 0x1AAF, 'Tai Tham' ],
134        [ 0x1B00 => 0x1B7F, 'Balinese' ],
135        [ 0x1B80 => 0x1BBF, 'Sundanese' ],
136        [ 0x1BC0 => 0x1BFF, 'Batak' ],
137        [ 0x1C00 => 0x1C4F, 'Lepcha' ],
138        [ 0x1C50 => 0x1C7F, 'Ol Chiki' ],
139        [ 0x1C80 => 0x1C8F, 'Cyrillic Extended-C' ],
140        [ 0x1C90 => 0x1CBF, 'Georgian Extended' ],
141        [ 0x1CC0 => 0x1CCF, 'Sundanese Supplement' ],
142        [ 0x1CD0 => 0x1CFF, 'Vedic Extensions' ],
143        [ 0x1F00 => 0x1FFF, 'Greek Extended' ],
144        [ 0x2C00 => 0x2C5F, 'Glagolitic' ],
145        [ 0x2C60 => 0x2C7F, 'Latin Extended-C' ],
146        [ 0x2C80 => 0x2CFF, 'Coptic' ],
147        [ 0x2D00 => 0x2D2F, 'Georgian Supplement' ],
148        [ 0x2D30 => 0x2D7F, 'Tifinagh' ],
149        [ 0x2D80 => 0x2DDF, 'Ethiopic Extended' ],
150        [ 0x2DE0 => 0x2DFF, 'Cyrillic Extended-A' ],
151        [ 0x2E80 => 0x2EFF, 'CJK Radicals Supplement' ],
152        [ 0x2F00 => 0x2FDF, 'Kangxi Radicals' ],
153        [ 0x2FF0 => 0x2FFF, 'Ideographic Description Characters' ],
154        [ 0x3000 => 0x303F, 'CJK Symbols and Punctuation' ],
155        [ 0x3040 => 0x309F, 'Hiragana' ],
156        [ 0x30A0 => 0x30FF, 'Katakana' ],
157        [ 0x3100 => 0x312F, 'Bopomofo' ],
158        [ 0x3130 => 0x318F, 'Hangul Compatibility Jamo' ],
159        [ 0x3190 => 0x319F, 'Kanbun' ],
160        [ 0x31A0 => 0x31BF, 'Bopomofo Extended' ],
161        [ 0x31C0 => 0x31EF, 'CJK Strokes' ],
162        [ 0x31F0 => 0x31FF, 'Katakana Phonetic Extensions' ],
163        [ 0x3200 => 0x32FF, 'Enclosed CJK Letters and Months' ],
164        [ 0x3300 => 0x33FF, 'CJK Compatibility' ],
165        [ 0x3400 => 0x4DBF, 'CJK Unified Ideographs Extension A' ],
166        [ 0x4DC0 => 0x4DFF, 'Yijing Hexagram Symbols' ],
167        [ 0x4E00 => 0x9FFF, 'CJK Unified Ideographs' ],
168        [ 0xA000 => 0xA48F, 'Yi Syllables' ],
169        [ 0xA490 => 0xA4CF, 'Yi Radicals' ],
170        [ 0xA4D0 => 0xA4FF, 'Lisu' ],
171        [ 0xA500 => 0xA63F, 'Vai' ],
172        [ 0xA640 => 0xA69F, 'Cyrillic Extended-B' ],
173        [ 0xA6A0 => 0xA6FF, 'Bamum' ],
174        [ 0xA800 => 0xA82F, 'Syloti Nagri' ],
175        [ 0xA830 => 0xA83F, 'Common Indic Number Forms' ],
176        [ 0xA840 => 0xA87F, 'Phags-pa' ],
177        [ 0xA880 => 0xA8DF, 'Saurashtra' ],
178        [ 0xA8E0 => 0xA8FF, 'Devanagari Extended' ],
179        [ 0xA900 => 0xA92F, 'Kayah Li' ],
180        [ 0xA930 => 0xA95F, 'Rejang' ],
181        [ 0xA960 => 0xA97F, 'Hangul Jamo Extended-A' ],
182        [ 0xA980 => 0xA9DF, 'Javanese' ],
183        [ 0xA9E0 => 0xA9FF, 'Myanmar Extended-B' ],
184        [ 0xAA00 => 0xAA5F, 'Cham' ],
185        [ 0xAA60 => 0xAA7F, 'Myanmar Extended-A' ],
186        [ 0xAA80 => 0xAADF, 'Tai Viet' ],
187        [ 0xAAE0 => 0xAAFF, 'Meetei Mayek Extensions' ],
188        [ 0xAB00 => 0xAB2F, 'Ethiopic Extended-A' ],
189        [ 0xAB70 => 0xABBF, 'Cherokee Supplement' ],
190        [ 0xABC0 => 0xABFF, 'Meetei Mayek' ],
191        [ 0xAC00 => 0xD7AF, 'Hangul Syllables' ],
192        [ 0xD7B0 => 0xD7FF, 'Hangul Jamo Extended-B' ],
193        [ 0xF900 => 0xFAFF, 'CJK Compatibility Ideographs' ],
194        [ 0xFB00 => 0xFB4F, 'Alphabetic Presentation Forms' ],
195        [ 0xFB50 => 0xFDFF, 'Arabic Presentation Forms-A' ],
196        [ 0xFE10 => 0xFE1F, 'Vertical Forms' ],
197    ];
198
199    my $_block_ranges = {};
200    for my $def (@$unicode_plane0_selected_blocks) {
201        my $block = $def->[2];
202        $result->{$block}->{char_count} += ($def->[1] - $def->[0] + 1);
203        push @{$_block_ranges->{$block}}, sprintf('\x{%x}-\x{%x}', $def->[0], $def->[1]);
204    }
205
206    for my $block (keys %$_block_ranges) {
207        my $list = sprintf '[%s]', join(",", @{ $_block_ranges->{$block} });
208        $result->{$block}->{regex} = qr/$list/;
209    }
210
211    my $char_count = 0;
212    $char_count += $_ for map { $_->{char_count} } values %{ $result };
213
214    $result->{OTHER()}->{char_count} = UNICODE_ASSIGNED_CHARACTERS - $char_count;
215
216    return $result;
217}
218
219
220after hook_register_checks => sub {
221    my $self = shift;
222    $self->register_check('entropy' => [ 50, 'check_entropy' ]);
223    $self->add_default_check('entropy');
224};
225
226
227sub check_entropy {
228    my $self = shift;
229    if ($self->_calc_entropy($self->password) < $self->min_entropy) {
230        return [ "entropy" => "I18N_OPENXPKI_UI_PASSWORD_QUALITY_INSUFFICIENT_ENTROPY" ];
231    }
232    return;
233}
234
235sub _calc_entropy {
236    my ($self, $passw) = @_;
237
238    return 0 unless (defined($passw) && $passw ne '');
239
240    my $entropy = 0;
241
242    my $classes = +{};
243
244    my $eff_len = 0.0;      # the effective length
245    my $char_count = +{};   # to count characters quantities
246    my $distances = +{};    # to collect differences between adjacent characters
247
248    my $len = length($passw);
249
250    my $prev_nc = 0;
251
252    for (my $i = 0; $i < $len; $i++) {
253        my $c = substr($passw, $i, 1);
254        my $nc = ord($c);
255        $classes->{$self->_get_char_class($c)} = 1;
256
257        my $incr = 1.0;     # value/factor for increment effective length
258
259        if ($i > 0) {
260            my $d = $nc - $prev_nc;
261
262            if (exists($distances->{$d})) {
263                $distances->{$d}++;
264                $incr /= $distances->{$d};
265            }
266            else {
267                $distances->{$d} = 1;
268            }
269        }
270
271        if (exists($char_count->{$c})) {
272            $char_count->{$c}++;
273            $eff_len += $incr * (1.0 / $char_count->{$c});
274        }
275        else {
276            $char_count->{$c} = 1;
277            $eff_len += $incr;
278        }
279
280        $prev_nc = $nc;
281    }
282
283    # printf "Validated passwort contains characters from these blocks: %s\n", join ", ", sort keys %$classes;
284
285    my $pci = 0; # Password complexity index
286    for (keys(%$classes)) {
287        $pci += $self->unicode_block($_)->{char_count};
288    }
289
290    if ($pci != 0) {
291        my $bits_per_char = log($pci) / log(2.0);
292        $entropy = floor($bits_per_char * $eff_len);
293    }
294    ##! 32: "Password entropy: $entropy"
295    return $entropy;
296}
297
298sub _get_char_class {
299    my ($self, $char) = @_;
300    for my $block (grep { $_ ne OTHER } $self->unicode_block_names) {
301        return $block if $char =~ $self->unicode_block($block)->{regex};
302    }
303    return OTHER;
304}
305
3061;
307