Skip to content

Commit ec52185

Browse files
authored
Merge pull request #8 from Avicted/fixes
feat: enforce username length validation in registration and login pr…
2 parents 7ea3ec0 + 4cb0925 commit ec52185

7 files changed

Lines changed: 131 additions & 16 deletions

File tree

cmd/client/chat.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -561,13 +561,16 @@ func (m *chatModel) refreshChannels(showMessage bool) {
561561
return
562562
}
563563
needsRefresh := false
564+
allEncrypted := true
564565
for _, ch := range channels {
565566
if _, err := m.ensureChannelKey(ch.ID); err != nil {
566567
needsRefresh = true
567568
}
568569
name := m.decryptChannelName(ch.ID, ch.NameEnc)
569570
if name == "<encrypted>" {
570571
needsRefresh = true
572+
} else {
573+
allEncrypted = false
571574
}
572575
m.channels[ch.ID] = channelInfo{ID: ch.ID, Name: name}
573576
}
@@ -583,6 +586,9 @@ func (m *chatModel) refreshChannels(showMessage bool) {
583586
b.WriteString(")")
584587
}
585588
m.appendSystemMessage(b.String())
589+
if allEncrypted {
590+
m.appendSystemMessage("All channels are <encrypted> because no other online user has shared channel keys yet. They will decrypt once another user comes online and shares keys (or an admin re-shares them).")
591+
}
586592
}
587593
m.ensureSidebarIndex()
588594
m.channelRefreshNeeded = needsRefresh
@@ -1207,6 +1213,8 @@ func (m *chatModel) renderMessages() string {
12071213
}
12081214
if msg.isSystem {
12091215
sender = "system"
1216+
} else {
1217+
sender = formatUsername(sender)
12101218
}
12111219

12121220
var style lipgloss.Style
@@ -1252,7 +1260,7 @@ func (m *chatModel) renderSidebar() string {
12521260
style = sidebarOfflineStyle
12531261
}
12541262
}
1255-
name := entry.Name
1263+
name := formatUsername(entry.Name)
12561264
if entry.Admin {
12571265
name = fmt.Sprintf("%s (admin)", name)
12581266
}
@@ -1278,7 +1286,7 @@ func (m *chatModel) renderSidebar() string {
12781286
style = sidebarOfflineStyle
12791287
}
12801288
}
1281-
name := entry.Name
1289+
name := formatUsername(entry.Name)
12821290
if entry.Admin {
12831291
name = fmt.Sprintf("%s (admin)", name)
12841292
}
@@ -1637,7 +1645,7 @@ func (m chatModel) View() string {
16371645
header := fmt.Sprintf(
16381646
" %s %s %s %s",
16391647
appNameStyle.Render("* dialtone"),
1640-
headerStyle.Render(m.auth.Username),
1648+
headerStyle.Render(formatUsername(m.auth.Username)),
16411649
labelStyle.Render(shortID(m.auth.UserID)),
16421650
labelStyle.Render(m.activeChannelLabel()),
16431651
)
@@ -1702,6 +1710,17 @@ func shortID(id string) string {
17021710
return id
17031711
}
17041712

1713+
func formatUsername(name string) string {
1714+
name = strings.TrimSpace(name)
1715+
if name == "" {
1716+
return name
1717+
}
1718+
if strings.HasPrefix(name, "<") && strings.HasSuffix(name, ">") {
1719+
return name
1720+
}
1721+
return "<" + name + ">"
1722+
}
1723+
17051724
func clampMin(v, minimum int) int {
17061725
if v < minimum {
17071726
return minimum

cmd/client/login.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ type loginModel struct {
2727
selectIndex int
2828
}
2929

30+
const (
31+
minUsernameLen = 2
32+
maxUsernameLen = 20
33+
)
34+
3035
func newLoginModel(defaultServer string) loginModel {
3136
server := textinput.New()
3237
server.Placeholder = "http://localhost:8080"
@@ -41,8 +46,8 @@ func newLoginModel(defaultServer string) loginModel {
4146
server.Focus()
4247

4348
username := textinput.New()
44-
username.Placeholder = "username"
45-
username.CharLimit = 64
49+
username.Placeholder = "username (2-20 chars)"
50+
username.CharLimit = maxUsernameLen
4651
username.Width = 30
4752

4853
password := textinput.New()
@@ -307,9 +312,13 @@ func (m loginModel) validateSubmit() string {
307312
if strings.TrimSpace(m.serverURL()) == "" {
308313
return "server url is required"
309314
}
310-
if m.username() == "" || m.password() == "" {
315+
username := strings.TrimSpace(m.username())
316+
if username == "" || m.password() == "" {
311317
return "username and password are required"
312318
}
319+
if len(username) < minUsernameLen || len(username) > maxUsernameLen {
320+
return fmt.Sprintf("username must be %d-%d characters", minUsernameLen, maxUsernameLen)
321+
}
313322
if len(m.passphrase()) < 8 {
314323
return "keystore passphrase must be at least 8 characters"
315324
}

cmd/client/login_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"strings"
45
"testing"
56

67
tea "github.com/charmbracelet/bubbletea"
@@ -15,6 +16,19 @@ func TestLoginValidateSubmit(t *testing.T) {
1516
t.Fatalf("unexpected error: %s", msg)
1617
}
1718

19+
m.usernameInput.SetValue("a")
20+
if msg := m.validateSubmit(); msg == "" {
21+
t.Fatalf("expected username length error")
22+
}
23+
24+
m.usernameInput.CharLimit = 0
25+
m.usernameInput.SetValue(strings.Repeat("a", 21))
26+
if msg := m.validateSubmit(); msg == "" {
27+
t.Fatalf("expected username max length error")
28+
}
29+
30+
m.usernameInput.SetValue("alice")
31+
1832
m.passphraseInp.SetValue("short")
1933
if msg := m.validateSubmit(); msg == "" {
2034
t.Fatalf("expected passphrase error")

internal/auth/auth.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ var (
2323
ErrTokenExpired = errors.New("token expired")
2424
)
2525

26+
const (
27+
minUsernameLen = 2
28+
maxUsernameLen = 20
29+
)
30+
2631
type Session struct {
2732
Token string
2833
UserID user.ID
@@ -59,8 +64,8 @@ func (s *Service) Register(ctx context.Context, username, password, publicKey, i
5964
return user.User{}, device.Device{}, Session{}, errors.New("invites service is required")
6065
}
6166
name := normalizeUsername(username)
62-
if name == "" {
63-
return user.User{}, device.Device{}, Session{}, ErrInvalidInput
67+
if err := validateUsername(name); err != nil {
68+
return user.User{}, device.Device{}, Session{}, err
6469
}
6570
if err := validateRegisterPassword(password); err != nil {
6671
return user.User{}, device.Device{}, Session{}, err
@@ -107,8 +112,8 @@ func (s *Service) Login(ctx context.Context, username, password, publicKey strin
107112
return user.User{}, device.Device{}, Session{}, errors.New("services are required")
108113
}
109114
name := normalizeUsername(username)
110-
if name == "" {
111-
return user.User{}, device.Device{}, Session{}, ErrInvalidInput
115+
if err := validateUsername(name); err != nil {
116+
return user.User{}, device.Device{}, Session{}, err
112117
}
113118
if strings.TrimSpace(password) == "" || len(password) < 8 {
114119
return user.User{}, device.Device{}, Session{}, ErrInvalidInput
@@ -219,6 +224,16 @@ func normalizeUsername(username string) string {
219224
return strings.ToLower(strings.TrimSpace(username))
220225
}
221226

227+
func validateUsername(name string) error {
228+
if name == "" {
229+
return ErrInvalidInput
230+
}
231+
if len(name) < minUsernameLen || len(name) > maxUsernameLen {
232+
return fmt.Errorf("%w: username must be %d-%d characters", ErrInvalidInput, minUsernameLen, maxUsernameLen)
233+
}
234+
return nil
235+
}
236+
222237
type tokenStore struct {
223238
mu sync.Mutex
224239
sessions map[string]Session

internal/auth/auth_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,23 @@ func TestRegister_EmptyUsername(t *testing.T) {
224224
}
225225
}
226226

227+
func TestRegister_ShortUsername(t *testing.T) {
228+
svc := newTestService()
229+
_, _, _, err := svc.Register(context.Background(), "a", "password123", "a2V5", "invite-1")
230+
if !errors.Is(err, ErrInvalidInput) {
231+
t.Fatalf("expected ErrInvalidInput, got %v", err)
232+
}
233+
}
234+
235+
func TestRegister_LongUsername(t *testing.T) {
236+
svc := newTestService()
237+
longName := strings.Repeat("a", 21)
238+
_, _, _, err := svc.Register(context.Background(), longName, "password123", "a2V5", "invite-1")
239+
if !errors.Is(err, ErrInvalidInput) {
240+
t.Fatalf("expected ErrInvalidInput, got %v", err)
241+
}
242+
}
243+
227244
func TestLogin_Success(t *testing.T) {
228245
svc := newTestService()
229246
ctx := context.Background()

internal/user/service.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import (
1414

1515
var ErrInvalidInput = errors.New("invalid input")
1616

17+
const (
18+
minUsernameLen = 2
19+
maxUsernameLen = 20
20+
)
21+
1722
type Service struct {
1823
repo Repository
1924
idGen func() ID
@@ -38,8 +43,8 @@ func (s *Service) Create(ctx context.Context, username string) (User, error) {
3843
}
3944

4045
name := normalizeUsername(username)
41-
if name == "" {
42-
return User{}, ErrInvalidInput
46+
if err := validateUsername(name); err != nil {
47+
return User{}, err
4348
}
4449
if len(s.pepper) == 0 {
4550
return User{}, errors.New("username pepper is required")
@@ -67,7 +72,10 @@ func (s *Service) CreateWithPassword(ctx context.Context, username, passwordHash
6772
}
6873

6974
name := normalizeUsername(username)
70-
if name == "" || strings.TrimSpace(passwordHash) == "" {
75+
if err := validateUsername(name); err != nil {
76+
return User{}, err
77+
}
78+
if strings.TrimSpace(passwordHash) == "" {
7179
return User{}, ErrInvalidInput
7280
}
7381
if len(s.pepper) == 0 {
@@ -98,7 +106,10 @@ func (s *Service) CreateWithPasswordAndID(ctx context.Context, id ID, username,
98106
return User{}, ErrInvalidInput
99107
}
100108
name := normalizeUsername(username)
101-
if name == "" || strings.TrimSpace(passwordHash) == "" {
109+
if err := validateUsername(name); err != nil {
110+
return User{}, err
111+
}
112+
if strings.TrimSpace(passwordHash) == "" {
102113
return User{}, ErrInvalidInput
103114
}
104115
if len(s.pepper) == 0 {
@@ -147,8 +158,8 @@ func (s *Service) GetByUsername(ctx context.Context, username string) (User, err
147158
return User{}, errors.New("repository is required")
148159
}
149160
name := normalizeUsername(username)
150-
if name == "" {
151-
return User{}, ErrInvalidInput
161+
if err := validateUsername(name); err != nil {
162+
return User{}, err
152163
}
153164
if len(s.pepper) == 0 {
154165
return User{}, errors.New("username pepper is required")
@@ -208,6 +219,16 @@ func normalizeUsername(username string) string {
208219
return strings.ToLower(strings.TrimSpace(username))
209220
}
210221

222+
func validateUsername(name string) error {
223+
if name == "" {
224+
return ErrInvalidInput
225+
}
226+
if len(name) < minUsernameLen || len(name) > maxUsernameLen {
227+
return ErrInvalidInput
228+
}
229+
return nil
230+
}
231+
211232
func hashUsername(pepper []byte, username string) string {
212233
mac := hmac.New(sha256.New, pepper)
213234
_, _ = mac.Write([]byte(username))

internal/user/service_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package user
33
import (
44
"context"
55
"errors"
6+
"strings"
67
"testing"
78
"time"
89
)
@@ -119,6 +120,25 @@ func TestCreate_WhitespaceUsername(t *testing.T) {
119120
}
120121
}
121122

123+
func TestCreate_ShortUsername(t *testing.T) {
124+
svc, _ := newTestService()
125+
126+
_, err := svc.Create(context.Background(), "a")
127+
if !errors.Is(err, ErrInvalidInput) {
128+
t.Fatalf("expected ErrInvalidInput, got %v", err)
129+
}
130+
}
131+
132+
func TestCreate_LongUsername(t *testing.T) {
133+
svc, _ := newTestService()
134+
longName := strings.Repeat("a", 21)
135+
136+
_, err := svc.Create(context.Background(), longName)
137+
if !errors.Is(err, ErrInvalidInput) {
138+
t.Fatalf("expected ErrInvalidInput, got %v", err)
139+
}
140+
}
141+
122142
func TestCreate_TrimsWhitespace(t *testing.T) {
123143
svc, _ := newTestService()
124144

0 commit comments

Comments
 (0)