Detailed changes
@@ -22,6 +22,7 @@ require (
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/knadh/go-pop3 v1.0.2
github.com/mattn/go-sixel v0.0.9
+ github.com/wagslane/go-password-validator v0.3.0
github.com/yuin/goldmark v1.8.2
github.com/yuin/gopher-lua v1.1.2
github.com/zalando/go-keyring v0.2.8
@@ -103,6 +103,8 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
+github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -174,6 +174,12 @@
"disable_confirm": "تعطيل التشفير؟",
"disable_warning": "سيتم تخزين جميع البيانات بدون تشفير.",
"encrypting": "جاري تشفير البيانات...",
+ "passwords_match": "✓ كلمات المرور متطابقة",
+ "passwords_do_not_match": "✗ كلمات المرور غير متطابقة",
+ "strength_label": "القوة:",
+ "strength_weak": "ضعيف",
+ "strength_medium": "متوسط",
+ "strength_strong": "قوي",
"error_empty": "لا يمكن أن تكون كلمة المرور فارغة",
"error_mismatch": "كلمات المرور غير متطابقة",
"help": "tab: التالي • enter: حفظ"
@@ -170,6 +170,12 @@
"disable_confirm": "Verschlüsselung deaktivieren?",
"disable_warning": "Alle Daten werden unverschlüsselt gespeichert.",
"encrypting": "Daten werden verschlüsselt...",
+ "passwords_match": "✓ Passwörter stimmen überein",
+ "passwords_do_not_match": "✗ Passwörter stimmen nicht überein",
+ "strength_label": "Stärke:",
+ "strength_weak": "schwach",
+ "strength_medium": "mittel",
+ "strength_strong": "stark",
"error_empty": "Passwort darf nicht leer sein",
"error_mismatch": "Passwörter stimmen nicht überein",
"help": "tab: nächstes • enter: speichern"
@@ -170,6 +170,12 @@
"disable_confirm": "Disable encryption?",
"disable_warning": "All data will be stored unencrypted.",
"encrypting": "Encrypting data...",
+ "passwords_match": "✓ Passwords match",
+ "passwords_do_not_match": "✗ Passwords do not match",
+ "strength_label": "Strength:",
+ "strength_weak": "weak",
+ "strength_medium": "medium",
+ "strength_strong": "strong",
"error_empty": "Password cannot be empty",
"error_mismatch": "Passwords do not match",
"help": "tab: next • enter: save"
@@ -170,6 +170,12 @@
"disable_confirm": "¿Deshabilitar cifrado?",
"disable_warning": "Todos los datos se almacenarán sin cifrar.",
"encrypting": "Cifrando datos...",
+ "passwords_match": "✓ Las contraseñas coinciden",
+ "passwords_do_not_match": "✗ Las contraseñas no coinciden",
+ "strength_label": "Fortaleza:",
+ "strength_weak": "débil",
+ "strength_medium": "media",
+ "strength_strong": "fuerte",
"error_empty": "La contraseña no puede estar vacía",
"error_mismatch": "Las contraseñas no coinciden",
"help": "tab: siguiente • enter: guardar"
@@ -170,6 +170,12 @@
"disable_confirm": "Désactiver le chiffrement ?",
"disable_warning": "Toutes les données seront stockées non chiffrées.",
"encrypting": "Chiffrement des données...",
+ "passwords_match": "✓ Les mots de passe correspondent",
+ "passwords_do_not_match": "✗ Les mots de passe ne correspondent pas",
+ "strength_label": "Force :",
+ "strength_weak": "faible",
+ "strength_medium": "moyen",
+ "strength_strong": "fort",
"error_empty": "Le mot de passe ne peut pas être vide",
"error_mismatch": "Les mots de passe ne correspondent pas",
"help": "tab: suivant • entrée: enregistrer"
@@ -168,6 +168,12 @@
"disable_confirm": "暗号化を無効にしますか?",
"disable_warning": "すべてのデータは暗号化されずに保存されます。",
"encrypting": "データを暗号化中...",
+ "passwords_match": "✓ パスワードが一致しました",
+ "passwords_do_not_match": "✗ パスワードが一致しません",
+ "strength_label": "強度:",
+ "strength_weak": "弱い",
+ "strength_medium": "普通",
+ "strength_strong": "強い",
"error_empty": "パスワードを空にすることはできません",
"error_mismatch": "パスワードが一致しません",
"help": "tab: 次へ • enter: 保存"
@@ -174,6 +174,12 @@
"disable_confirm": "Wyłączyć szyfrowanie?",
"disable_warning": "Wszystkie dane będą przechowywane bez szyfrowania.",
"encrypting": "Szyfrowanie danych...",
+ "passwords_match": "✓ Hasła pasują do siebie",
+ "passwords_do_not_match": "✗ Hasła nie pasują do siebie",
+ "strength_label": "Siła:",
+ "strength_weak": "słabe",
+ "strength_medium": "średnie",
+ "strength_strong": "silne",
"error_empty": "Hasło nie może być puste",
"error_mismatch": "Hasła nie pasują do siebie",
"help": "tab: następny • enter: zapisz"
@@ -170,6 +170,12 @@
"disable_confirm": "Desativar criptografia?",
"disable_warning": "Todos os dados serão armazenados sem criptografia.",
"encrypting": "Criptografando dados...",
+ "passwords_match": "✓ As senhas coincidem",
+ "passwords_do_not_match": "✗ As senhas não coincidem",
+ "strength_label": "Força:",
+ "strength_weak": "fraca",
+ "strength_medium": "média",
+ "strength_strong": "forte",
"error_empty": "A senha não pode estar vazia",
"error_mismatch": "As senhas não coincidem",
"help": "tab: próximo • enter: salvar"
@@ -174,6 +174,12 @@
"disable_confirm": "Отключить шифрование?",
"disable_warning": "Все данные будут храниться незашифрованными.",
"encrypting": "Шифрование данных...",
+ "passwords_match": "✓ Пароли совпадают",
+ "passwords_do_not_match": "✗ Пароли не совпадают",
+ "strength_label": "Надёжность:",
+ "strength_weak": "слабый",
+ "strength_medium": "средний",
+ "strength_strong": "сильный",
"error_empty": "Пароль не может быть пустым",
"error_mismatch": "Пароли не совпадают",
"help": "tab: следующий • enter: сохранить"
@@ -172,6 +172,12 @@
"disable_confirm": "Вимкнути шифрування?",
"disable_warning": "Всі дані будуть зберігатися без шифрування.",
"encrypting": "Шифрування даних...",
+ "passwords_match": "✓ Паролі співпадають",
+ "passwords_do_not_match": "✗ Паролі не співпадають",
+ "strength_label": "Надійність:",
+ "strength_weak": "слабкий",
+ "strength_medium": "середній",
+ "strength_strong": "сильний",
"error_empty": "Пароль не може бути порожнім",
"error_mismatch": "Паролі не співпадають",
"help": "tab: далі • enter: зберегти"
@@ -168,6 +168,12 @@
"disable_confirm": "禁用加密?",
"disable_warning": "所有数据将以未加密方式存储。",
"encrypting": "正在加密数据...",
+ "passwords_match": "✓ 密码匹配",
+ "passwords_do_not_match": "✗ 密码不匹配",
+ "strength_label": "强度:",
+ "strength_weak": "弱",
+ "strength_medium": "中等",
+ "strength_strong": "强",
"error_empty": "密码不能为空",
"error_mismatch": "密码不匹配",
"help": "tab: 下一项 • enter: 保存"
@@ -0,0 +1,21 @@
+package passwordstrength
+
+import passwordvalidator "github.com/wagslane/go-password-validator"
+
+type LibMeter struct{}
+
+func NewLibMeter() LibMeter {
+ return LibMeter{}
+}
+
+func (m LibMeter) Strength(password string) Strength {
+ entropy := passwordvalidator.GetEntropy(password)
+ switch {
+ case entropy >= strongEntropyBits:
+ return Strong
+ case entropy >= mediumEntropyBits:
+ return Medium
+ default:
+ return Weak
+ }
+}
@@ -0,0 +1,18 @@
+package passwordstrength
+
+type Strength string
+
+const (
+ Weak Strength = "weak"
+ Medium Strength = "medium"
+ Strong Strength = "strong"
+)
+
+const (
+ mediumEntropyBits = 50
+ strongEntropyBits = 70
+)
+
+type Meter interface {
+ Strength(password string) Strength
+}
@@ -7,6 +7,7 @@ import (
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/floatpane/matcha/config"
+ "github.com/floatpane/matcha/internal/passwordstrength"
"github.com/floatpane/matcha/plugin"
"github.com/floatpane/matcha/theme"
)
@@ -16,6 +17,7 @@ var (
selectedAccountItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("42")).Bold(true)
accountEmailStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
dangerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196"))
+ successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
settingsFocusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Bold(true)
settingsBlurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
@@ -69,12 +71,14 @@ type Settings struct {
pgpPINInput textinput.Model
// Encryption fields
- encPasswordInput textinput.Model
- encConfirmInput textinput.Model
- encFocusIndex int
- encError string
- encEnabling bool
- confirmingDisable bool
+ encPasswordInput textinput.Model
+ encConfirmInput textinput.Model
+ encFocusIndex int
+ encError string
+ encEnabling bool
+ confirmingDisable bool
+ passwordMeter passwordstrength.Meter
+ encPasswordStrength passwordstrength.Strength
// Plugin settings state
plugins *plugin.Manager
@@ -130,6 +134,7 @@ func NewSettings(cfg *config.Config) *Settings {
pgpKeySource: "file",
encPasswordInput: newInput("Password", "> ", true),
encConfirmInput: newInput("Confirm Password", "> ", true),
+ passwordMeter: passwordstrength.NewLibMeter(),
pluginInput: newInput("", "> ", false),
}
}
@@ -292,6 +297,7 @@ func (m *Settings) updateMenu(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
m.encError = ""
m.encPasswordInput.SetValue("")
m.encConfirmInput.SetValue("")
+ m.encPasswordStrength = ""
m.encFocusIndex = 0
m.confirmingDisable = false
m.encEnabling = false
@@ -6,6 +6,7 @@ import (
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/floatpane/matcha/config"
+ "github.com/floatpane/matcha/internal/passwordstrength"
)
func (m *Settings) updateEncryption(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
@@ -38,6 +39,7 @@ func (m *Settings) updateEncryption(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
// Clear inputs and return to menu
m.encPasswordInput.SetValue("")
m.encConfirmInput.SetValue("")
+ m.encPasswordStrength = ""
m.encPasswordInput.Blur()
m.encConfirmInput.Blur()
m.encError = ""
@@ -98,7 +100,11 @@ func (m *Settings) updateEncryption(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
// Forward input to focused textinput
var cmd tea.Cmd
if m.encFocusIndex == 0 {
+ before := m.encPasswordInput.Value()
m.encPasswordInput, cmd = m.encPasswordInput.Update(msg)
+ if m.encPasswordInput.Value() != before {
+ m.handlePasswordChanged()
+ }
} else if m.encFocusIndex == 1 {
m.encConfirmInput, cmd = m.encConfirmInput.Update(msg)
}
@@ -137,13 +143,20 @@ func (m *Settings) viewEncryption() string {
b.WriteString(settingsBlurredStyle.Render(t("settings_encryption.password_label") + "\n"))
}
b.WriteString(m.encPasswordInput.View() + "\n\n")
+ if m.encPasswordStrength != "" {
+ b.WriteString(" " + m.renderPasswordStrength() + "\n\n")
+ }
if m.encFocusIndex == 1 {
b.WriteString(settingsFocusedStyle.Render(t("settings_encryption.confirm_label") + "\n"))
} else {
b.WriteString(settingsBlurredStyle.Render(t("settings_encryption.confirm_label") + "\n"))
}
- b.WriteString(m.encConfirmInput.View() + "\n\n")
+ b.WriteString(m.encConfirmInput.View() + "\n")
+ if status := m.renderPasswordMatch(); status != "" {
+ b.WriteString(" " + status + "\n")
+ }
+ b.WriteString("\n")
saveBtn := "[ " + t("settings_encryption.enable_button") + " ]"
if m.encFocusIndex == 2 {
@@ -165,3 +178,35 @@ func (m *Settings) viewEncryption() string {
return b.String()
}
+
+func (m *Settings) renderPasswordMatch() string {
+ password := m.encPasswordInput.Value()
+ confirm := m.encConfirmInput.Value()
+ if confirm == "" {
+ return ""
+ }
+ if password == confirm {
+ return successStyle.Render(t("settings_encryption.passwords_match"))
+ }
+ return dangerStyle.Render(t("settings_encryption.passwords_do_not_match"))
+}
+
+func (m *Settings) handlePasswordChanged() {
+ password := m.encPasswordInput.Value()
+ if password == "" {
+ m.encPasswordStrength = ""
+ return
+ }
+ m.encPasswordStrength = m.passwordMeter.Strength(password)
+}
+
+func (m *Settings) renderPasswordStrength() string {
+ switch m.encPasswordStrength {
+ case passwordstrength.Strong:
+ return successStyle.Render(t("settings_encryption.strength_label") + " " + t("settings_encryption.strength_strong"))
+ case passwordstrength.Medium:
+ return settingsFocusedStyle.Render(t("settings_encryption.strength_label") + " " + t("settings_encryption.strength_medium"))
+ default:
+ return dangerStyle.Render(t("settings_encryption.strength_label") + " " + t("settings_encryption.strength_weak"))
+ }
+}