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