Skip to content

Commit b5cf5d6

Browse files
committed
Add a passcode strength meter over vendored zxcvbn
Task: 2026/08/31/passcode-strength-meter
1 parent f1f3ad1 commit b5cf5d6

11 files changed

Lines changed: 521 additions & 0 deletions

File tree

‎.gitmodules‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,6 @@
103103
[submodule "Telegram/ThirdParty/libfido2"]
104104
path = Telegram/ThirdParty/libfido2
105105
url = https://github.com/Yubico/libfido2.git
106+
[submodule "Telegram/ThirdParty/zxcvbn"]
107+
path = Telegram/ThirdParty/zxcvbn
108+
url = https://github.com/tsyrogit/zxcvbn-c.git

‎Telegram/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ include(cmake/lib_stripe.cmake)
3030
include(cmake/lib_tgcalls.cmake)
3131
include(cmake/lib_prisma.cmake)
3232
include(cmake/lib_fido2.cmake)
33+
include(cmake/lib_zxcvbn.cmake)
3334
include(cmake/td_export.cmake)
3435
include(cmake/td_gram.cmake)
3536
include(cmake/td_iv.cmake)

‎Telegram/Resources/langs/lang.strings‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9035,4 +9035,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
90359035
"lng_folder_archive_failed" = "Failed to create archive.";
90369036
"lng_folder_archive_status" = "ZIP archive";
90379037

9038+
"lng_passcode_strength_very_weak" = "Very weak";
9039+
"lng_passcode_strength_weak" = "Weak";
9040+
"lng_passcode_strength_good" = "Good";
9041+
"lng_passcode_strength_strong" = "Strong";
9042+
"lng_passcode_strength_advice_short" = "Use at least 8 characters.";
9043+
"lng_passcode_strength_advice_word" = "Avoid common words and names.";
9044+
"lng_passcode_strength_advice_leet" = "Swapping letters for lookalike symbols doesn't help.";
9045+
"lng_passcode_strength_advice_sequence" = "Avoid runs like abc or 123.";
9046+
"lng_passcode_strength_advice_repeat" = "Avoid repeated characters.";
9047+
"lng_passcode_strength_advice_keyboard" = "Avoid keyboard patterns.";
9048+
"lng_passcode_strength_advice_date" = "Avoid dates and years.";
9049+
"lng_passcode_strength_advice_variety" = "Make it longer and mix in more kinds of characters.";
9050+
"lng_passcode_strength_advice_fine" = "This passcode is hard to guess.";
9051+
90389052
// Keys finished
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
This file is part of Telegram Desktop,
3+
the official desktop application for the Telegram messaging service.
4+
5+
For license and copyright information please follow this link:
6+
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7+
*/
8+
#include "ui/passcode_strength.h"
9+
10+
#include <gsl/gsl>
11+
12+
#include <zxcvbn.h>
13+
14+
namespace Ui {
15+
namespace {
16+
17+
constexpr auto kMinimumCharacters = 8;
18+
constexpr auto kBitsPerDecimalDigit = 3.321928094887362;
19+
constexpr auto kBitsForThousandGuesses = 3 * kBitsPerDecimalDigit;
20+
constexpr auto kBitsForMillionGuesses = 6 * kBitsPerDecimalDigit;
21+
constexpr auto kBitsForHundredMillionGuesses = 8 * kBitsPerDecimalDigit;
22+
constexpr auto kBitsForTenBillionGuesses = 10 * kBitsPerDecimalDigit;
23+
24+
[[nodiscard]] int CountCharacters(const QString &passcode) {
25+
const auto size = int(passcode.size());
26+
auto result = 0;
27+
for (auto i = 0; i != size; ++i) {
28+
++result;
29+
if (passcode[i].isHighSurrogate()
30+
&& (i + 1 != size)
31+
&& passcode[i + 1].isLowSurrogate()) {
32+
++i;
33+
}
34+
}
35+
return result;
36+
}
37+
38+
[[nodiscard]] PasscodeStrengthBand BandFromBits(double bits) {
39+
const auto score = (bits < kBitsForThousandGuesses)
40+
? 0
41+
: (bits < kBitsForMillionGuesses)
42+
? 1
43+
: (bits < kBitsForHundredMillionGuesses)
44+
? 2
45+
: (bits < kBitsForTenBillionGuesses)
46+
? 3
47+
: 4;
48+
return (score < 2)
49+
? PasscodeStrengthBand::Weak
50+
: (score < 4)
51+
? PasscodeStrengthBand::Good
52+
: PasscodeStrengthBand::Strong;
53+
}
54+
55+
[[nodiscard]] PasscodeStrengthAdvice AdviceFromMatches(ZxcMatch_t *info) {
56+
auto longest = (ZxcMatch_t*)nullptr;
57+
for (auto part = info; part != nullptr; part = part->Next) {
58+
if (!longest) {
59+
longest = part;
60+
continue;
61+
}
62+
const auto better = (part->Length != longest->Length)
63+
? (part->Length > longest->Length)
64+
: (part->Entrpy != longest->Entrpy)
65+
? (part->Entrpy < longest->Entrpy)
66+
: (part->Begin < longest->Begin);
67+
if (better) {
68+
longest = part;
69+
}
70+
}
71+
if (!longest) {
72+
return PasscodeStrengthAdvice::AddVariety;
73+
}
74+
switch (ZxcTypeMatch_t(longest->Type & 31)) {
75+
case DICTIONARY_MATCH:
76+
case USER_MATCH:
77+
return PasscodeStrengthAdvice::CommonWord;
78+
case DICT_LEET_MATCH:
79+
case USER_LEET_MATCH:
80+
return PasscodeStrengthAdvice::LeetWord;
81+
case SEQUENCE_MATCH:
82+
return PasscodeStrengthAdvice::Sequence;
83+
case REPEATS_MATCH:
84+
return PasscodeStrengthAdvice::Repeat;
85+
case SPATIAL_MATCH:
86+
return PasscodeStrengthAdvice::Keyboard;
87+
case DATE_MATCH:
88+
case YEAR_MATCH:
89+
return PasscodeStrengthAdvice::DateLike;
90+
case NON_MATCH:
91+
case BRUTE_MATCH:
92+
case LONG_PWD_MATCH:
93+
case MULTIPLE_MATCH:
94+
return PasscodeStrengthAdvice::AddVariety;
95+
}
96+
return PasscodeStrengthAdvice::AddVariety;
97+
}
98+
99+
void WipeBytes(QByteArray &bytes) {
100+
const auto size = int(bytes.size());
101+
const auto data = static_cast<volatile char*>(bytes.data());
102+
for (auto i = 0; i != size; ++i) {
103+
data[i] = 0;
104+
}
105+
bytes.clear();
106+
}
107+
108+
} // namespace
109+
110+
PasscodeStrength EstimatePasscodeStrength(const QString &passcode) {
111+
if (CountCharacters(passcode) < kMinimumCharacters) {
112+
return {
113+
PasscodeStrengthBand::VeryWeak,
114+
PasscodeStrengthAdvice::TooShort,
115+
};
116+
}
117+
auto utf8 = passcode.toUtf8();
118+
auto info = (ZxcMatch_t*)nullptr;
119+
const auto guard = gsl::finally([&] {
120+
ZxcvbnFreeInfo(info);
121+
WipeBytes(utf8);
122+
});
123+
const auto bits = ZxcvbnMatch(utf8.constData(), nullptr, &info);
124+
const auto band = BandFromBits(bits);
125+
return {
126+
band,
127+
(band == PasscodeStrengthBand::Strong)
128+
? PasscodeStrengthAdvice::Fine
129+
: AdviceFromMatches(info),
130+
};
131+
}
132+
133+
} // namespace Ui
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
This file is part of Telegram Desktop,
3+
the official desktop application for the Telegram messaging service.
4+
5+
For license and copyright information please follow this link:
6+
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7+
*/
8+
#pragma once
9+
10+
#include <QtCore/QString>
11+
12+
namespace Ui {
13+
14+
enum class PasscodeStrengthBand : uchar {
15+
VeryWeak,
16+
Weak,
17+
Good,
18+
Strong,
19+
};
20+
21+
enum class PasscodeStrengthAdvice : uchar {
22+
TooShort,
23+
CommonWord,
24+
LeetWord,
25+
Sequence,
26+
Repeat,
27+
Keyboard,
28+
DateLike,
29+
AddVariety,
30+
Fine,
31+
};
32+
33+
struct PasscodeStrength {
34+
PasscodeStrengthBand band = PasscodeStrengthBand::VeryWeak;
35+
PasscodeStrengthAdvice advice = PasscodeStrengthAdvice::TooShort;
36+
};
37+
38+
[[nodiscard]] PasscodeStrength EstimatePasscodeStrength(
39+
const QString &passcode);
40+
41+
} // namespace Ui
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
This file is part of Telegram Desktop,
3+
the official desktop application for the Telegram messaging service.
4+
5+
For license and copyright information please follow this link:
6+
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7+
*/
8+
#include "ui/widgets/passcode_strength_meter.h"
9+
10+
#include "lang/lang_keys.h"
11+
#include "ui/widgets/labels.h"
12+
#include "ui/painter.h"
13+
#include "ui/qt_object_factory.h"
14+
15+
#include "styles/style_passcode_strength_meter.h"
16+
17+
namespace Ui {
18+
namespace {
19+
20+
constexpr auto kSegmentCount = int(PasscodeStrengthBand::Strong) + 1;
21+
22+
[[nodiscard]] rpl::producer<QString> AdviceText(
23+
PasscodeStrengthAdvice advice) {
24+
switch (advice) {
25+
case PasscodeStrengthAdvice::TooShort:
26+
return tr::lng_passcode_strength_advice_short();
27+
case PasscodeStrengthAdvice::CommonWord:
28+
return tr::lng_passcode_strength_advice_word();
29+
case PasscodeStrengthAdvice::LeetWord:
30+
return tr::lng_passcode_strength_advice_leet();
31+
case PasscodeStrengthAdvice::Sequence:
32+
return tr::lng_passcode_strength_advice_sequence();
33+
case PasscodeStrengthAdvice::Repeat:
34+
return tr::lng_passcode_strength_advice_repeat();
35+
case PasscodeStrengthAdvice::Keyboard:
36+
return tr::lng_passcode_strength_advice_keyboard();
37+
case PasscodeStrengthAdvice::DateLike:
38+
return tr::lng_passcode_strength_advice_date();
39+
case PasscodeStrengthAdvice::AddVariety:
40+
return tr::lng_passcode_strength_advice_variety();
41+
case PasscodeStrengthAdvice::Fine:
42+
return tr::lng_passcode_strength_advice_fine();
43+
}
44+
Unexpected("Advice in AdviceText.");
45+
}
46+
47+
[[nodiscard]] const style::color &BandColor(
48+
const style::PasscodeStrengthMeter &st,
49+
PasscodeStrengthBand band) {
50+
switch (band) {
51+
case PasscodeStrengthBand::VeryWeak: return st.veryWeakFg;
52+
case PasscodeStrengthBand::Weak: return st.weakFg;
53+
case PasscodeStrengthBand::Good: return st.goodFg;
54+
case PasscodeStrengthBand::Strong: return st.strongFg;
55+
}
56+
Unexpected("Band in BandColor.");
57+
}
58+
59+
} // namespace
60+
61+
PasscodeStrengthMeter::PasscodeStrengthMeter(
62+
QWidget *parent,
63+
const style::PasscodeStrengthMeter &st)
64+
: RpWidget(parent)
65+
, _st(st)
66+
, _label(CreateChild<FlatLabel>(this, _st.label)) {
67+
_label->heightValue() | rpl::on_next([=] {
68+
if (!_inResize) {
69+
resizeToWidth(width());
70+
}
71+
}, lifetime());
72+
}
73+
74+
void PasscodeStrengthMeter::showCandidate(const QString &candidate) {
75+
_empty = candidate.isEmpty();
76+
_strength = EstimatePasscodeStrength(candidate);
77+
_adviceLifetime.destroy();
78+
if (_empty) {
79+
_label->setText(QString());
80+
} else {
81+
AdviceText(_strength.advice) | rpl::on_next([=](
82+
const QString &text) {
83+
_label->setText(text);
84+
}, _adviceLifetime);
85+
}
86+
update();
87+
}
88+
89+
int PasscodeStrengthMeter::resizeGetHeight(int newWidth) {
90+
_inResize = true;
91+
_label->resizeToWidth(newWidth);
92+
_label->moveToLeft(0, _st.height + _st.labelSkip, newWidth);
93+
_inResize = false;
94+
95+
const auto labelHeight = std::max(
96+
_label->height(),
97+
_st.label.style.font->height);
98+
return _st.height + _st.labelSkip + labelHeight;
99+
}
100+
101+
void PasscodeStrengthMeter::paintEvent(QPaintEvent *e) {
102+
auto p = QPainter(this);
103+
auto hq = PainterHighQualityEnabler(p);
104+
105+
p.setPen(Qt::NoPen);
106+
107+
const auto spacing = _st.segmentSpacing;
108+
const auto available = width() - spacing * (kSegmentCount - 1);
109+
const auto bandFg = _empty
110+
? _st.emptyFg
111+
: BandColor(_st, _strength.band);
112+
const auto filled = _empty ? 0 : (int(_strength.band) + 1);
113+
auto left = 0;
114+
for (auto i = 0; i != kSegmentCount; ++i) {
115+
const auto segment = (available * (i + 1) / kSegmentCount)
116+
- (available * i / kSegmentCount);
117+
p.setBrush((i < filled) ? bandFg : _st.emptyFg);
118+
p.drawRoundedRect(
119+
QRect(left, 0, segment, _st.height),
120+
_st.segmentRadius,
121+
_st.segmentRadius);
122+
left += segment + spacing;
123+
}
124+
}
125+
126+
rpl::producer<QString> PasscodeStrengthBandName(PasscodeStrengthBand band) {
127+
switch (band) {
128+
case PasscodeStrengthBand::VeryWeak:
129+
return tr::lng_passcode_strength_very_weak();
130+
case PasscodeStrengthBand::Weak:
131+
return tr::lng_passcode_strength_weak();
132+
case PasscodeStrengthBand::Good:
133+
return tr::lng_passcode_strength_good();
134+
case PasscodeStrengthBand::Strong:
135+
return tr::lng_passcode_strength_strong();
136+
}
137+
Unexpected("Band in PasscodeStrengthBandName.");
138+
}
139+
140+
const style::color &PasscodeStrengthBandColor(PasscodeStrengthBand band) {
141+
return BandColor(st::defaultPasscodeStrengthMeter, band);
142+
}
143+
144+
} // namespace Ui

0 commit comments

Comments
 (0)