zxcvbn-pl 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zxcvbn_pl-0.1.0/.gitignore +9 -0
- zxcvbn_pl-0.1.0/CHANGELOG.md +7 -0
- zxcvbn_pl-0.1.0/LICENSE +24 -0
- zxcvbn_pl-0.1.0/NOTICE.md +37 -0
- zxcvbn_pl-0.1.0/PKG-INFO +152 -0
- zxcvbn_pl-0.1.0/README.md +123 -0
- zxcvbn_pl-0.1.0/pyproject.toml +59 -0
- zxcvbn_pl-0.1.0/scripts/build_dictionaries.py +104 -0
- zxcvbn_pl-0.1.0/src/zxcvbn_pl/__init__.py +78 -0
- zxcvbn_pl-0.1.0/src/zxcvbn_pl/data/pl_common_words.marisa +0 -0
- zxcvbn_pl-0.1.0/src/zxcvbn_pl/data/pl_first_names.marisa +0 -0
- zxcvbn_pl-0.1.0/src/zxcvbn_pl/data/pl_last_names.marisa +0 -0
- zxcvbn_pl-0.1.0/src/zxcvbn_pl/py.typed +0 -0
- zxcvbn_pl-0.1.0/tests/test_zxcvbn_pl.py +78 -0
zxcvbn_pl-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michał Pasternak, IPLweb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
The bundled dictionaries in src/zxcvbn_pl/data are derived from third-party
|
|
24
|
+
data under their own licenses (ODC-BY, CC0 1.0) — see NOTICE.md.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Data sources and licenses
|
|
2
|
+
|
|
3
|
+
The code of zxcvbn-pl is licensed under the MIT License (see `LICENSE`).
|
|
4
|
+
The dictionaries in `src/zxcvbn_pl/data/*.marisa` are derived from the
|
|
5
|
+
[`@zxcvbn-ts/language-pl`](https://github.com/zxcvbn-ts/zxcvbn/tree/master/packages/languages/pl)
|
|
6
|
+
npm package, version 4.1.1 (MIT), and carry the licenses of their sources:
|
|
7
|
+
|
|
8
|
+
## `pl_common_words.marisa`
|
|
9
|
+
|
|
10
|
+
Derived from OpenSubtitles via OPUS (`commonWords.json` in
|
|
11
|
+
`@zxcvbn-ts/language-pl`).
|
|
12
|
+
|
|
13
|
+
- Source dataset: OpenSubtitles 2024, https://opus.nlpl.eu/OpenSubtitles.php
|
|
14
|
+
- Provider: Helsinki-NLP / OPUS
|
|
15
|
+
- License: **ODC-BY** (Open Data Commons Attribution License),
|
|
16
|
+
https://opendatacommons.org/licenses/by/
|
|
17
|
+
- Original data is attributed to subtitle contributors and rights holders
|
|
18
|
+
(http://www.opensubtitles.org/).
|
|
19
|
+
|
|
20
|
+
This package contains a processed derivative: tokenized text, frequency
|
|
21
|
+
aggregation, normalization and filtering (by zxcvbn-ts), then lowercasing,
|
|
22
|
+
dropping entries shorter than 3 characters or containing non-letters, adding
|
|
23
|
+
diacritics-free variants and converting to a marisa-trie index (by
|
|
24
|
+
zxcvbn-pl). Any redistribution must retain this notice.
|
|
25
|
+
|
|
26
|
+
## `pl_first_names.marisa`, `pl_last_names.marisa`
|
|
27
|
+
|
|
28
|
+
Derived from the lists of first names and last names in the Polish PESEL
|
|
29
|
+
registry, published by the Ministry of Digital Affairs on dane.gov.pl:
|
|
30
|
+
|
|
31
|
+
- Lista imion występujących w rejestrze PESEL,
|
|
32
|
+
https://dane.gov.pl/pl/dataset/1667 — **CC0 1.0**
|
|
33
|
+
- Nazwiska występujące w rejestrze PESEL,
|
|
34
|
+
https://dane.gov.pl/pl/dataset/1681 — **CC0 1.0**
|
|
35
|
+
|
|
36
|
+
zxcvbn-ts keeps names with at least 200 occurrences; zxcvbn-pl merges the
|
|
37
|
+
male and female lists, lowercases them and adds diacritics-free variants.
|
zxcvbn_pl-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: zxcvbn-pl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Polish dictionaries (common words, first and last names) for zxcvbn password strength estimation.
|
|
5
|
+
Project-URL: Homepage, https://github.com/iplweb/zxcvbn-pl
|
|
6
|
+
Project-URL: Repository, https://github.com/iplweb/zxcvbn-pl
|
|
7
|
+
Project-URL: Changelog, https://github.com/iplweb/zxcvbn-pl/blob/main/CHANGELOG.md
|
|
8
|
+
Author-email: Michał Pasternak <m@iplweb.pl>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
License-File: NOTICE.md
|
|
12
|
+
Keywords: dictionary,hasło,password,password-strength,polish,security,zxcvbn
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Natural Language :: Polish
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Security
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Requires-Dist: marisa-trie>=1.0
|
|
27
|
+
Requires-Dist: zxcvbn>=4.4
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# zxcvbn-pl
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/zxcvbn-pl/)
|
|
33
|
+
[](https://github.com/iplweb/zxcvbn-pl/actions/workflows/ci.yml)
|
|
34
|
+
[](LICENSE)
|
|
35
|
+
|
|
36
|
+
Polskie słowniki dla [zxcvbn](https://github.com/dwolfhub/zxcvbn-python)
|
|
37
|
+
(port w Pythonie) — pospolite słowa, imiona i nazwiska.
|
|
38
|
+
|
|
39
|
+
*Polish dictionaries for the Python port of zxcvbn — common words, first
|
|
40
|
+
names and last names. [English below.](#english)*
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 🇵🇱 Po polsku
|
|
45
|
+
|
|
46
|
+
### Po co
|
|
47
|
+
|
|
48
|
+
zxcvbn szacuje, ile prób potrzeba do odgadnięcia hasła, rozpoznając w nim
|
|
49
|
+
słowa ze słowników, powtórzenia, sekwencje, układy klawiatury i daty. Ma
|
|
50
|
+
jednak **tylko angielskie słowniki** — polskie słowo to dla niego losowe
|
|
51
|
+
litery. Przez to hasła łatwe do zgadnięcia dla każdego, kto ma polską listę
|
|
52
|
+
słów, dostają najwyższą ocenę:
|
|
53
|
+
|
|
54
|
+
| Hasło | zxcvbn | zxcvbn + zxcvbn-pl |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `słoneczko123456` | 4 | 2 |
|
|
57
|
+
| `pomidorowa123456` | 3 | 2 |
|
|
58
|
+
| `Katarzyna1985!` | 3 | 2 |
|
|
59
|
+
| `agnieszka.nowak1` | 4 | 3 |
|
|
60
|
+
| `Wojciechowski77` | 2 | 1 |
|
|
61
|
+
| `ptaszek-na-drzewie` | 4 | 4 |
|
|
62
|
+
| `zielonaherbatazcytryna` | 4 | 4 |
|
|
63
|
+
|
|
64
|
+
Frazy z kilku słów nadal są oceniane wysoko — tak jak zaleca NIST
|
|
65
|
+
SP 800-63B. Spada ocena haseł z jednego pospolitego słowa, imienia lub
|
|
66
|
+
nazwiska z dopiskiem. Efekt jest umiarkowany: słowa spoza ok. 50 tys.
|
|
67
|
+
najczęstszych (np. `kaczuszka2000!!`) nadal są dla zxcvbn losowymi literami.
|
|
68
|
+
|
|
69
|
+
Port zxcvbn w JavaScripcie (zxcvbn-ts) ma pakiety językowe; port w Pythonie
|
|
70
|
+
— nie. Ten pakiet wypełnia tę lukę danymi z `@zxcvbn-ts/language-pl`.
|
|
71
|
+
|
|
72
|
+
### Instalacja i użycie
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install zxcvbn-pl
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import zxcvbn_pl
|
|
80
|
+
from zxcvbn import zxcvbn
|
|
81
|
+
|
|
82
|
+
zxcvbn_pl.register() # raz, np. przy starcie aplikacji; wielokrotne wywołanie nic nie psuje
|
|
83
|
+
|
|
84
|
+
wynik = zxcvbn("słoneczko123456")
|
|
85
|
+
wynik["score"] # 2
|
|
86
|
+
[m.get("dictionary_name") for m in wynik["sequence"]]
|
|
87
|
+
# ['pl_common_words', 'passwords']
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`register()` dokłada trzy słowniki do globalnego rejestru zxcvbn
|
|
91
|
+
(`zxcvbn.matching.RANKED_DICTIONARIES`), więc działa dla każdego kolejnego
|
|
92
|
+
wywołania `zxcvbn()` w procesie:
|
|
93
|
+
|
|
94
|
+
| Nazwa (`dictionary_name`) | Zawartość |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `pl_common_words` | ok. 52 tys. najczęstszych słów (napisy filmowe) |
|
|
97
|
+
| `pl_first_names` | imiona z rejestru PESEL (≥ 200 osób) |
|
|
98
|
+
| `pl_last_names` | nazwiska z rejestru PESEL (≥ 200 osób) |
|
|
99
|
+
|
|
100
|
+
Każde słowo z polskimi znakami jest też w wariancie bez nich (`żółw` →
|
|
101
|
+
`zolw`) z tą samą pozycją. Pomijane są wpisy krótsze niż 3 znaki.
|
|
102
|
+
|
|
103
|
+
Słowniki to indeksy [marisa-trie](https://github.com/pytries/marisa-trie)
|
|
104
|
+
czytane przez mmap (~1 MB na dysku, znikomy narzut RAM).
|
|
105
|
+
|
|
106
|
+
### Uwaga o wątkach
|
|
107
|
+
|
|
108
|
+
zxcvbn trzyma słowniki — także `user_inputs` przekazane do `zxcvbn()` —
|
|
109
|
+
w globalnym słowniku modułu. `register()` jest bezpieczne, ale jeśli
|
|
110
|
+
wywołujesz `zxcvbn(..., user_inputs=...)` z wielu wątków, owiń wywołanie
|
|
111
|
+
blokadą (to ograniczenie samego zxcvbn, nie tego pakietu).
|
|
112
|
+
|
|
113
|
+
### Dane i licencje
|
|
114
|
+
|
|
115
|
+
Kod: MIT. Dane pochodzą z `@zxcvbn-ts/language-pl` i mają własne licencje —
|
|
116
|
+
szczegóły w [NOTICE.md](NOTICE.md):
|
|
117
|
+
|
|
118
|
+
- pospolite słowa: OPUS OpenSubtitles 2024 — **ODC-BY** (wymaga podania źródła),
|
|
119
|
+
- imiona i nazwiska: rejestr PESEL, dane.gov.pl — **CC0 1.0**.
|
|
120
|
+
|
|
121
|
+
Odbudowa słowników (deterministyczna, z weryfikacją sha512 paczki npm):
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
uv run python scripts/build_dictionaries.py
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## English
|
|
130
|
+
|
|
131
|
+
zxcvbn ships with English dictionaries only, so Polish words look like random
|
|
132
|
+
letters to it and passwords such as `słoneczko123456` or `Katarzyna1985!`
|
|
133
|
+
get the top score. `zxcvbn-pl` adds Polish common words, first names and last
|
|
134
|
+
names (plus diacritics-free variants) to the Python port of zxcvbn:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
import zxcvbn_pl
|
|
138
|
+
from zxcvbn import zxcvbn
|
|
139
|
+
|
|
140
|
+
zxcvbn_pl.register()
|
|
141
|
+
zxcvbn("słoneczko123456")["score"] # 2 instead of 4
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`register()` is idempotent and adds three dictionaries (`pl_common_words`,
|
|
145
|
+
`pl_first_names`, `pl_last_names`) to zxcvbn's global
|
|
146
|
+
`zxcvbn.matching.RANKED_DICTIONARIES`. Multi-word passphrases still score
|
|
147
|
+
high, in line with NIST SP 800-63B.
|
|
148
|
+
|
|
149
|
+
The data comes from the `@zxcvbn-ts/language-pl` npm package: common words
|
|
150
|
+
from OPUS OpenSubtitles 2024 (ODC-BY), first and last names from the Polish
|
|
151
|
+
PESEL registry published on dane.gov.pl (CC0 1.0). See [NOTICE.md](NOTICE.md).
|
|
152
|
+
The code is MIT-licensed.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# zxcvbn-pl
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/zxcvbn-pl/)
|
|
4
|
+
[](https://github.com/iplweb/zxcvbn-pl/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Polskie słowniki dla [zxcvbn](https://github.com/dwolfhub/zxcvbn-python)
|
|
8
|
+
(port w Pythonie) — pospolite słowa, imiona i nazwiska.
|
|
9
|
+
|
|
10
|
+
*Polish dictionaries for the Python port of zxcvbn — common words, first
|
|
11
|
+
names and last names. [English below.](#english)*
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 🇵🇱 Po polsku
|
|
16
|
+
|
|
17
|
+
### Po co
|
|
18
|
+
|
|
19
|
+
zxcvbn szacuje, ile prób potrzeba do odgadnięcia hasła, rozpoznając w nim
|
|
20
|
+
słowa ze słowników, powtórzenia, sekwencje, układy klawiatury i daty. Ma
|
|
21
|
+
jednak **tylko angielskie słowniki** — polskie słowo to dla niego losowe
|
|
22
|
+
litery. Przez to hasła łatwe do zgadnięcia dla każdego, kto ma polską listę
|
|
23
|
+
słów, dostają najwyższą ocenę:
|
|
24
|
+
|
|
25
|
+
| Hasło | zxcvbn | zxcvbn + zxcvbn-pl |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `słoneczko123456` | 4 | 2 |
|
|
28
|
+
| `pomidorowa123456` | 3 | 2 |
|
|
29
|
+
| `Katarzyna1985!` | 3 | 2 |
|
|
30
|
+
| `agnieszka.nowak1` | 4 | 3 |
|
|
31
|
+
| `Wojciechowski77` | 2 | 1 |
|
|
32
|
+
| `ptaszek-na-drzewie` | 4 | 4 |
|
|
33
|
+
| `zielonaherbatazcytryna` | 4 | 4 |
|
|
34
|
+
|
|
35
|
+
Frazy z kilku słów nadal są oceniane wysoko — tak jak zaleca NIST
|
|
36
|
+
SP 800-63B. Spada ocena haseł z jednego pospolitego słowa, imienia lub
|
|
37
|
+
nazwiska z dopiskiem. Efekt jest umiarkowany: słowa spoza ok. 50 tys.
|
|
38
|
+
najczęstszych (np. `kaczuszka2000!!`) nadal są dla zxcvbn losowymi literami.
|
|
39
|
+
|
|
40
|
+
Port zxcvbn w JavaScripcie (zxcvbn-ts) ma pakiety językowe; port w Pythonie
|
|
41
|
+
— nie. Ten pakiet wypełnia tę lukę danymi z `@zxcvbn-ts/language-pl`.
|
|
42
|
+
|
|
43
|
+
### Instalacja i użycie
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install zxcvbn-pl
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import zxcvbn_pl
|
|
51
|
+
from zxcvbn import zxcvbn
|
|
52
|
+
|
|
53
|
+
zxcvbn_pl.register() # raz, np. przy starcie aplikacji; wielokrotne wywołanie nic nie psuje
|
|
54
|
+
|
|
55
|
+
wynik = zxcvbn("słoneczko123456")
|
|
56
|
+
wynik["score"] # 2
|
|
57
|
+
[m.get("dictionary_name") for m in wynik["sequence"]]
|
|
58
|
+
# ['pl_common_words', 'passwords']
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`register()` dokłada trzy słowniki do globalnego rejestru zxcvbn
|
|
62
|
+
(`zxcvbn.matching.RANKED_DICTIONARIES`), więc działa dla każdego kolejnego
|
|
63
|
+
wywołania `zxcvbn()` w procesie:
|
|
64
|
+
|
|
65
|
+
| Nazwa (`dictionary_name`) | Zawartość |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `pl_common_words` | ok. 52 tys. najczęstszych słów (napisy filmowe) |
|
|
68
|
+
| `pl_first_names` | imiona z rejestru PESEL (≥ 200 osób) |
|
|
69
|
+
| `pl_last_names` | nazwiska z rejestru PESEL (≥ 200 osób) |
|
|
70
|
+
|
|
71
|
+
Każde słowo z polskimi znakami jest też w wariancie bez nich (`żółw` →
|
|
72
|
+
`zolw`) z tą samą pozycją. Pomijane są wpisy krótsze niż 3 znaki.
|
|
73
|
+
|
|
74
|
+
Słowniki to indeksy [marisa-trie](https://github.com/pytries/marisa-trie)
|
|
75
|
+
czytane przez mmap (~1 MB na dysku, znikomy narzut RAM).
|
|
76
|
+
|
|
77
|
+
### Uwaga o wątkach
|
|
78
|
+
|
|
79
|
+
zxcvbn trzyma słowniki — także `user_inputs` przekazane do `zxcvbn()` —
|
|
80
|
+
w globalnym słowniku modułu. `register()` jest bezpieczne, ale jeśli
|
|
81
|
+
wywołujesz `zxcvbn(..., user_inputs=...)` z wielu wątków, owiń wywołanie
|
|
82
|
+
blokadą (to ograniczenie samego zxcvbn, nie tego pakietu).
|
|
83
|
+
|
|
84
|
+
### Dane i licencje
|
|
85
|
+
|
|
86
|
+
Kod: MIT. Dane pochodzą z `@zxcvbn-ts/language-pl` i mają własne licencje —
|
|
87
|
+
szczegóły w [NOTICE.md](NOTICE.md):
|
|
88
|
+
|
|
89
|
+
- pospolite słowa: OPUS OpenSubtitles 2024 — **ODC-BY** (wymaga podania źródła),
|
|
90
|
+
- imiona i nazwiska: rejestr PESEL, dane.gov.pl — **CC0 1.0**.
|
|
91
|
+
|
|
92
|
+
Odbudowa słowników (deterministyczna, z weryfikacją sha512 paczki npm):
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
uv run python scripts/build_dictionaries.py
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## English
|
|
101
|
+
|
|
102
|
+
zxcvbn ships with English dictionaries only, so Polish words look like random
|
|
103
|
+
letters to it and passwords such as `słoneczko123456` or `Katarzyna1985!`
|
|
104
|
+
get the top score. `zxcvbn-pl` adds Polish common words, first names and last
|
|
105
|
+
names (plus diacritics-free variants) to the Python port of zxcvbn:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import zxcvbn_pl
|
|
109
|
+
from zxcvbn import zxcvbn
|
|
110
|
+
|
|
111
|
+
zxcvbn_pl.register()
|
|
112
|
+
zxcvbn("słoneczko123456")["score"] # 2 instead of 4
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`register()` is idempotent and adds three dictionaries (`pl_common_words`,
|
|
116
|
+
`pl_first_names`, `pl_last_names`) to zxcvbn's global
|
|
117
|
+
`zxcvbn.matching.RANKED_DICTIONARIES`. Multi-word passphrases still score
|
|
118
|
+
high, in line with NIST SP 800-63B.
|
|
119
|
+
|
|
120
|
+
The data comes from the `@zxcvbn-ts/language-pl` npm package: common words
|
|
121
|
+
from OPUS OpenSubtitles 2024 (ODC-BY), first and last names from the Polish
|
|
122
|
+
PESEL registry published on dane.gov.pl (CC0 1.0). See [NOTICE.md](NOTICE.md).
|
|
123
|
+
The code is MIT-licensed.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "zxcvbn-pl"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Polish dictionaries (common words, first and last names) for zxcvbn password strength estimation."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Michał Pasternak", email = "m@iplweb.pl" }]
|
|
9
|
+
keywords = ["zxcvbn", "password", "password-strength", "polish", "dictionary", "hasło", "security"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Natural Language :: Polish",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.9",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
22
|
+
"Topic :: Security",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"marisa-trie>=1.0",
|
|
26
|
+
"zxcvbn>=4.4",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/iplweb/zxcvbn-pl"
|
|
31
|
+
Repository = "https://github.com/iplweb/zxcvbn-pl"
|
|
32
|
+
Changelog = "https://github.com/iplweb/zxcvbn-pl/blob/main/CHANGELOG.md"
|
|
33
|
+
|
|
34
|
+
[dependency-groups]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=8.0",
|
|
37
|
+
"ruff>=0.6",
|
|
38
|
+
"pre-commit>=3.5",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["hatchling"]
|
|
43
|
+
build-backend = "hatchling.build"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/zxcvbn_pl"]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.sdist]
|
|
49
|
+
only-include = ["src", "tests", "scripts", "README.md", "LICENSE", "NOTICE.md", "CHANGELOG.md"]
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 88
|
|
53
|
+
target-version = "py39"
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = ["E", "F", "W", "I", "B", "UP"]
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Build the Polish zxcvbn dictionaries shipped in ``src/zxcvbn_pl/data``.
|
|
2
|
+
|
|
3
|
+
Source data: the ``@zxcvbn-ts/language-pl`` npm package (pinned version,
|
|
4
|
+
verified against the registry's sha512 integrity). Each upstream list is
|
|
5
|
+
ordered by frequency; the position becomes the zxcvbn rank. Male and female
|
|
6
|
+
lists are merged (lower rank wins). For every word with Polish diacritics a
|
|
7
|
+
variant without them is added with the same rank (``żółw`` -> ``zolw``) —
|
|
8
|
+
that is how such words are usually typed in passwords.
|
|
9
|
+
|
|
10
|
+
Run from the repository root (needs only the runtime dependency)::
|
|
11
|
+
|
|
12
|
+
uv run python scripts/build_dictionaries.py
|
|
13
|
+
|
|
14
|
+
The output is deterministic: the same upstream version gives byte-identical
|
|
15
|
+
``.marisa`` files.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import base64
|
|
19
|
+
import hashlib
|
|
20
|
+
import io
|
|
21
|
+
import json
|
|
22
|
+
import tarfile
|
|
23
|
+
import unicodedata
|
|
24
|
+
import urllib.request
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
import marisa_trie
|
|
28
|
+
|
|
29
|
+
UPSTREAM_VERSION = "4.1.1"
|
|
30
|
+
UPSTREAM_URL = (
|
|
31
|
+
"https://registry.npmjs.org/@zxcvbn-ts/language-pl/-/"
|
|
32
|
+
f"language-pl-{UPSTREAM_VERSION}.tgz"
|
|
33
|
+
)
|
|
34
|
+
UPSTREAM_SHA512 = (
|
|
35
|
+
"hF6Qu9cyHx7sSEzNOQrJQntq8geoincsGvlOC0wkD43LnUVSTR49MKxdsifsemlQgtxR7aUK"
|
|
36
|
+
"DgK/e/RFHOljoQ=="
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Output dictionary name -> upstream lists merged into it.
|
|
40
|
+
DICTIONARIES = {
|
|
41
|
+
"pl_common_words": ["commonWords"],
|
|
42
|
+
"pl_first_names": ["femaleFirstnames", "maleFirstnames"],
|
|
43
|
+
"pl_last_names": ["femaleLastnames", "maleLastnames"],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Single letters and two-letter tokens match everywhere and only add noise.
|
|
47
|
+
MIN_LENGTH = 3
|
|
48
|
+
|
|
49
|
+
OUTPUT_DIR = Path(__file__).resolve().parent.parent / "src" / "zxcvbn_pl" / "data"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def strip_diacritics(word):
|
|
53
|
+
word = word.replace("ł", "l")
|
|
54
|
+
word = unicodedata.normalize("NFKD", word)
|
|
55
|
+
return "".join(c for c in word if not unicodedata.combining(c))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def download():
|
|
59
|
+
with urllib.request.urlopen(UPSTREAM_URL, timeout=60) as response:
|
|
60
|
+
payload = response.read()
|
|
61
|
+
digest = base64.b64encode(hashlib.sha512(payload).digest()).decode()
|
|
62
|
+
if digest != UPSTREAM_SHA512:
|
|
63
|
+
raise RuntimeError(
|
|
64
|
+
f"sha512 mismatch for {UPSTREAM_URL}: got {digest}, "
|
|
65
|
+
f"expected {UPSTREAM_SHA512}"
|
|
66
|
+
)
|
|
67
|
+
return payload
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def read_lists(payload):
|
|
71
|
+
lists = {}
|
|
72
|
+
with tarfile.open(fileobj=io.BytesIO(payload), mode="r:gz") as tar:
|
|
73
|
+
for name in {n for names in DICTIONARIES.values() for n in names}:
|
|
74
|
+
member = tar.extractfile(f"package/src/{name}.json")
|
|
75
|
+
lists[name] = json.load(member)
|
|
76
|
+
return lists
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def build_ranks(upstream_lists):
|
|
80
|
+
ranks = {}
|
|
81
|
+
for words in upstream_lists:
|
|
82
|
+
for position, word in enumerate(words, start=1):
|
|
83
|
+
word = word.strip().lower()
|
|
84
|
+
if len(word) < MIN_LENGTH or not word.isalpha():
|
|
85
|
+
continue
|
|
86
|
+
for variant in (word, strip_diacritics(word)):
|
|
87
|
+
if position < ranks.get(variant, position + 1):
|
|
88
|
+
ranks[variant] = position
|
|
89
|
+
return ranks
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main():
|
|
93
|
+
lists = read_lists(download())
|
|
94
|
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
for dictionary, sources in DICTIONARIES.items():
|
|
96
|
+
ranks = build_ranks(lists[source] for source in sources)
|
|
97
|
+
trie = marisa_trie.RecordTrie("<I", ((w, (r,)) for w, r in ranks.items()))
|
|
98
|
+
path = OUTPUT_DIR / f"{dictionary}.marisa"
|
|
99
|
+
trie.save(str(path))
|
|
100
|
+
print(f"{path.name}: {len(trie)} entries, {path.stat().st_size // 1024} KB")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
main()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Polish dictionaries for zxcvbn (the Python port).
|
|
2
|
+
|
|
3
|
+
zxcvbn ships English dictionaries only, so to zxcvbn a Polish word looks like
|
|
4
|
+
random letters and passwords such as ``słoneczko123456`` or
|
|
5
|
+
``Katarzyna1985!`` get the top score. This package adds Polish common words,
|
|
6
|
+
first names and last names (with diacritics-free variants) to zxcvbn::
|
|
7
|
+
|
|
8
|
+
import zxcvbn_pl
|
|
9
|
+
from zxcvbn import zxcvbn
|
|
10
|
+
|
|
11
|
+
zxcvbn_pl.register()
|
|
12
|
+
zxcvbn("słoneczko123456")["score"] # 2 instead of 4
|
|
13
|
+
|
|
14
|
+
The dictionaries are marisa-trie indices read via mmap (no big RAM cost).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import threading
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import marisa_trie
|
|
21
|
+
|
|
22
|
+
__all__ = ["DICTIONARY_NAMES", "MarisaRankedDictionary", "dictionaries", "register"]
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
|
|
25
|
+
DATA_DIR = Path(__file__).parent / "data"
|
|
26
|
+
|
|
27
|
+
#: Names under which the dictionaries are registered in zxcvbn — they show up
|
|
28
|
+
#: as ``dictionary_name`` in zxcvbn's match sequence.
|
|
29
|
+
DICTIONARY_NAMES = ("pl_common_words", "pl_first_names", "pl_last_names")
|
|
30
|
+
|
|
31
|
+
_lock = threading.Lock()
|
|
32
|
+
_dictionaries = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MarisaRankedDictionary:
|
|
36
|
+
"""Read-only ``word -> rank`` mapping over a marisa-trie index.
|
|
37
|
+
|
|
38
|
+
zxcvbn only needs ``word in d`` and ``d[word]`` from a ranked dictionary,
|
|
39
|
+
so this is a drop-in replacement for the plain dicts it builds itself.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, path):
|
|
43
|
+
self._trie = marisa_trie.RecordTrie("<I").mmap(str(path))
|
|
44
|
+
|
|
45
|
+
def __contains__(self, word):
|
|
46
|
+
return word in self._trie
|
|
47
|
+
|
|
48
|
+
def __getitem__(self, word):
|
|
49
|
+
return self._trie[word][0][0]
|
|
50
|
+
|
|
51
|
+
def __len__(self):
|
|
52
|
+
return len(self._trie)
|
|
53
|
+
|
|
54
|
+
def get(self, word, default=None):
|
|
55
|
+
return self[word] if word in self else default
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def dictionaries():
|
|
59
|
+
"""Return ``{name: MarisaRankedDictionary}`` for all Polish dictionaries."""
|
|
60
|
+
global _dictionaries
|
|
61
|
+
with _lock:
|
|
62
|
+
if _dictionaries is None:
|
|
63
|
+
_dictionaries = {
|
|
64
|
+
name: MarisaRankedDictionary(DATA_DIR / f"{name}.marisa")
|
|
65
|
+
for name in DICTIONARY_NAMES
|
|
66
|
+
}
|
|
67
|
+
return _dictionaries
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def register():
|
|
71
|
+
"""Add the Polish dictionaries to zxcvbn. Safe to call many times.
|
|
72
|
+
|
|
73
|
+
zxcvbn keeps its ranked dictionaries in a module-level dict, so this
|
|
74
|
+
affects every later ``zxcvbn()`` call in the process.
|
|
75
|
+
"""
|
|
76
|
+
from zxcvbn import matching
|
|
77
|
+
|
|
78
|
+
matching.RANKED_DICTIONARIES.update(dictionaries())
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from zxcvbn import matching, zxcvbn
|
|
3
|
+
|
|
4
|
+
import zxcvbn_pl
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.fixture
|
|
8
|
+
def zarejestrowane():
|
|
9
|
+
przed = dict(matching.RANKED_DICTIONARIES)
|
|
10
|
+
zxcvbn_pl.register()
|
|
11
|
+
yield
|
|
12
|
+
matching.RANKED_DICTIONARIES.clear()
|
|
13
|
+
matching.RANKED_DICTIONARIES.update(przed)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_slowniki_maja_dane():
|
|
17
|
+
slowniki = zxcvbn_pl.dictionaries()
|
|
18
|
+
assert set(slowniki) == set(zxcvbn_pl.DICTIONARY_NAMES)
|
|
19
|
+
assert all(len(slownik) > 1000 for slownik in slowniki.values())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.parametrize(
|
|
23
|
+
"nazwa,slowo",
|
|
24
|
+
[
|
|
25
|
+
("pl_common_words", "kot"),
|
|
26
|
+
("pl_first_names", "katarzyna"),
|
|
27
|
+
("pl_last_names", "kowalski"),
|
|
28
|
+
("pl_last_names", "kowalska"),
|
|
29
|
+
],
|
|
30
|
+
)
|
|
31
|
+
def test_slowo_w_slowniku(nazwa, slowo):
|
|
32
|
+
assert slowo in zxcvbn_pl.dictionaries()[nazwa]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_wariant_bez_polskich_znakow_ma_te_sama_pozycje():
|
|
36
|
+
slownik = zxcvbn_pl.dictionaries()["pl_common_words"]
|
|
37
|
+
assert slownik["zolw"] == slownik["żółw"]
|
|
38
|
+
assert slownik["slonce"] == slownik["słońce"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_czestsze_slowo_ma_nizsza_pozycje():
|
|
42
|
+
slownik = zxcvbn_pl.dictionaries()["pl_common_words"]
|
|
43
|
+
assert slownik["nie"] < slownik["kot"] < slownik["żółw"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_brak_krotkich_i_nieliterowych_wpisow():
|
|
47
|
+
slownik = zxcvbn_pl.dictionaries()["pl_common_words"]
|
|
48
|
+
assert "na" not in slownik
|
|
49
|
+
assert "xqzvw" not in slownik
|
|
50
|
+
assert slownik.get("xqzvw") is None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_register_jest_idempotentne(zarejestrowane):
|
|
54
|
+
zxcvbn_pl.register()
|
|
55
|
+
for nazwa in zxcvbn_pl.DICTIONARY_NAMES:
|
|
56
|
+
assert matching.RANKED_DICTIONARIES[nazwa] is zxcvbn_pl.dictionaries()[nazwa]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.parametrize(
|
|
60
|
+
"haslo,slownik",
|
|
61
|
+
[
|
|
62
|
+
("słoneczko123456", "pl_common_words"),
|
|
63
|
+
("Katarzyna1985!", "pl_first_names"),
|
|
64
|
+
("Wojciechowski77", "pl_last_names"),
|
|
65
|
+
],
|
|
66
|
+
)
|
|
67
|
+
def test_polskie_slowa_obnizaja_ocene(zarejestrowane, haslo, slownik):
|
|
68
|
+
wynik = zxcvbn(haslo)
|
|
69
|
+
assert wynik["score"] <= 2
|
|
70
|
+
assert slownik in [m.get("dictionary_name") for m in wynik["sequence"]]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_bez_rejestracji_zxcvbn_nie_zna_polskich_slow():
|
|
74
|
+
assert zxcvbn("słoneczko123456")["score"] == 4
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_fraza_z_kilku_slow_nadal_mocna(zarejestrowane):
|
|
78
|
+
assert zxcvbn("zielonaherbatazcytryna")["score"] == 4
|