Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
6 changes: 6 additions & 0 deletions i18n/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -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: حفظ"
Expand Down
6 changes: 6 additions & 0 deletions i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -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: 保存"
Expand Down
6 changes: 6 additions & 0 deletions i18n/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions i18n/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -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: сохранить"
Expand Down
6 changes: 6 additions & 0 deletions i18n/locales/uk.json
Original file line number Diff line number Diff line change
Expand Up @@ -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: зберегти"
Expand Down
6 changes: 6 additions & 0 deletions i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -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: 保存"
Expand Down
21 changes: 21 additions & 0 deletions internal/passwordstrength/lib_meter.go
Original file line number Diff line number Diff line change
@@ -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
}
}
18 changes: 18 additions & 0 deletions internal/passwordstrength/meter.go
Original file line number Diff line number Diff line change
@@ -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
}
18 changes: 12 additions & 6 deletions tui/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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"))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
}
}
Expand Down Expand Up @@ -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
Expand Down
47 changes: 46 additions & 1 deletion tui/settings_encryption.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 = ""
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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"))
}
}
Loading