Browse Source

Merge pull request #334 from slingamn/confusables.4

implement confusables prevention (#178)
tags/v1.0.0-rc1
Daniel Oaks 5 years ago
parent
commit
1f33ad290c
No account linked to committer's email address
12 changed files with 285 additions and 77 deletions
  1. 9
    0
      Gopkg.lock
  2. 4
    0
      Gopkg.toml
  3. 96
    34
      irc/accounts.go
  4. 20
    18
      irc/client.go
  5. 38
    8
      irc/client_lookup_set.go
  6. 9
    6
      irc/getters.go
  7. 0
    1
      irc/handlers.go
  8. 3
    3
      irc/idletimer.go
  9. 1
    1
      irc/nickserv.go
  10. 57
    5
      irc/strings.go
  11. 47
    0
      irc/strings_test.go
  12. 1
    1
      vendor

+ 9
- 0
Gopkg.lock View File

@@ -61,6 +61,13 @@
61 61
   pruneopts = "UT"
62 62
   revision = "9520e82c474b0a04dd04f8a40959027271bab992"
63 63
 
64
+[[projects]]
65
+  digest = "1:7caf3ea977a13cd8b9a2e1ecef1ccaa8e38f831b4f6ffcb8bd0aa909c48afb3a"
66
+  name = "github.com/oragono/confusables"
67
+  packages = ["."]
68
+  pruneopts = "UT"
69
+  revision = "d5dd03409482fae2457f0742be22782890f720c2"
70
+
64 71
 [[projects]]
65 72
   branch = "master"
66 73
   digest = "1:2251e6a17ea4a6eaa708882a1cda837aae3e425edbb190ef39b761ecf15a5c3d"
@@ -199,12 +206,14 @@
199 206
     "github.com/goshuirc/irc-go/ircmsg",
200 207
     "github.com/mattn/go-colorable",
201 208
     "github.com/mgutz/ansi",
209
+    "github.com/oragono/confusables",
202 210
     "github.com/oragono/go-ident",
203 211
     "github.com/tidwall/buntdb",
204 212
     "golang.org/x/crypto/bcrypt",
205 213
     "golang.org/x/crypto/sha3",
206 214
     "golang.org/x/crypto/ssh/terminal",
207 215
     "golang.org/x/text/secure/precis",
216
+    "golang.org/x/text/unicode/norm",
208 217
     "gopkg.in/yaml.v2",
209 218
   ]
210 219
   solver-name = "gps-cdcl"

+ 4
- 0
Gopkg.toml View File

@@ -49,6 +49,10 @@
49 49
   branch = "master"
50 50
   name = "github.com/oragono/go-ident"
51 51
 
52
+[[constraint]]
53
+  revision = "d5dd03409482fae2457f0742be22782890f720c2"
54
+  name = "github.com/oragono/confusables"
55
+
52 56
 [[constraint]]
53 57
   name = "github.com/tidwall/buntdb"
54 58
   version = "1.0.0"

+ 96
- 34
irc/accounts.go View File

@@ -52,17 +52,19 @@ type AccountManager struct {
52 52
 
53 53
 	server *Server
54 54
 	// track clients logged in to accounts
55
-	accountToClients map[string][]*Client
56
-	nickToAccount    map[string]string
57
-	accountToMethod  map[string]NickReservationMethod
55
+	accountToClients  map[string][]*Client
56
+	nickToAccount     map[string]string
57
+	skeletonToAccount map[string]string
58
+	accountToMethod   map[string]NickReservationMethod
58 59
 }
59 60
 
60 61
 func NewAccountManager(server *Server) *AccountManager {
61 62
 	am := AccountManager{
62
-		accountToClients: make(map[string][]*Client),
63
-		nickToAccount:    make(map[string]string),
64
-		accountToMethod:  make(map[string]NickReservationMethod),
65
-		server:           server,
63
+		accountToClients:  make(map[string][]*Client),
64
+		nickToAccount:     make(map[string]string),
65
+		skeletonToAccount: make(map[string]string),
66
+		accountToMethod:   make(map[string]NickReservationMethod),
67
+		server:            server,
66 68
 	}
67 69
 
68 70
 	am.buildNickToAccountIndex()
@@ -76,6 +78,7 @@ func (am *AccountManager) buildNickToAccountIndex() {
76 78
 	}
77 79
 
78 80
 	nickToAccount := make(map[string]string)
81
+	skeletonToAccount := make(map[string]string)
79 82
 	accountToMethod := make(map[string]NickReservationMethod)
80 83
 	existsPrefix := fmt.Sprintf(keyAccountExists, "")
81 84
 
@@ -91,11 +94,21 @@ func (am *AccountManager) buildNickToAccountIndex() {
91 94
 			account := strings.TrimPrefix(key, existsPrefix)
92 95
 			if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, account)); err == nil {
93 96
 				nickToAccount[account] = account
97
+				accountName, err := tx.Get(fmt.Sprintf(keyAccountName, account))
98
+				if err != nil {
99
+					am.server.logger.Error("internal", "missing account name for", account)
100
+				} else {
101
+					skeleton, _ := Skeleton(accountName)
102
+					skeletonToAccount[skeleton] = account
103
+				}
94 104
 			}
95 105
 			if rawNicks, err := tx.Get(fmt.Sprintf(keyAccountAdditionalNicks, account)); err == nil {
96 106
 				additionalNicks := unmarshalReservedNicks(rawNicks)
97 107
 				for _, nick := range additionalNicks {
98
-					nickToAccount[nick] = account
108
+					cfnick, _ := CasefoldName(nick)
109
+					nickToAccount[cfnick] = account
110
+					skeleton, _ := Skeleton(nick)
111
+					skeletonToAccount[skeleton] = account
99 112
 				}
100 113
 			}
101 114
 
@@ -115,6 +128,7 @@ func (am *AccountManager) buildNickToAccountIndex() {
115 128
 	} else {
116 129
 		am.Lock()
117 130
 		am.nickToAccount = nickToAccount
131
+		am.skeletonToAccount = skeletonToAccount
118 132
 		am.accountToMethod = accountToMethod
119 133
 		am.Unlock()
120 134
 	}
@@ -171,36 +185,55 @@ func (am *AccountManager) NickToAccount(nick string) string {
171 185
 
172 186
 // Given a nick, looks up the account that owns it and the method (none/timeout/strict)
173 187
 // used to enforce ownership.
174
-func (am *AccountManager) EnforcementStatus(nick string) (account string, method NickReservationMethod) {
175
-	cfnick, err := CasefoldName(nick)
176
-	if err != nil {
177
-		return
178
-	}
179
-
188
+func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account string, method NickReservationMethod) {
180 189
 	config := am.server.Config()
181 190
 	if !config.Accounts.NickReservation.Enabled {
182
-		method = NickReservationNone
183
-		return
191
+		return "", NickReservationNone
184 192
 	}
185 193
 
186 194
 	am.RLock()
187 195
 	defer am.RUnlock()
188 196
 
189
-	account = am.nickToAccount[cfnick]
190
-	if account == "" {
191
-		method = NickReservationNone
197
+	// given an account, combine stored enforcement method with the config settings
198
+	// to compute the actual enforcement method
199
+	finalEnforcementMethod := func(account_ string) (result NickReservationMethod) {
200
+		result = am.accountToMethod[account_]
201
+		// if they don't have a custom setting, or customization is disabled, use the default
202
+		if result == NickReservationOptional || !config.Accounts.NickReservation.AllowCustomEnforcement {
203
+			result = config.Accounts.NickReservation.Method
204
+		}
205
+		if result == NickReservationOptional {
206
+			// enforcement was explicitly enabled neither in the config or by the user
207
+			result = NickReservationNone
208
+		}
192 209
 		return
193 210
 	}
194
-	method = am.accountToMethod[account]
195
-	// if they don't have a custom setting, or customization is disabled, use the default
196
-	if method == NickReservationOptional || !config.Accounts.NickReservation.AllowCustomEnforcement {
197
-		method = config.Accounts.NickReservation.Method
198
-	}
199
-	if method == NickReservationOptional {
200
-		// enforcement was explicitly enabled neither in the config or by the user
201
-		method = NickReservationNone
211
+
212
+	nickAccount := am.nickToAccount[cfnick]
213
+	skelAccount := am.skeletonToAccount[skeleton]
214
+	if nickAccount == "" && skelAccount == "" {
215
+		return "", NickReservationNone
216
+	} else if nickAccount != "" && (skelAccount == nickAccount || skelAccount == "") {
217
+		return nickAccount, finalEnforcementMethod(nickAccount)
218
+	} else if skelAccount != "" && nickAccount == "" {
219
+		return skelAccount, finalEnforcementMethod(skelAccount)
220
+	} else {
221
+		// nickAccount != skelAccount and both are nonempty:
222
+		// two people have competing claims on (this casefolding of) this nick!
223
+		nickMethod := finalEnforcementMethod(nickAccount)
224
+		skelMethod := finalEnforcementMethod(skelAccount)
225
+		switch {
226
+		case nickMethod == NickReservationNone && skelMethod == NickReservationNone:
227
+			return nickAccount, NickReservationNone
228
+		case skelMethod == NickReservationNone:
229
+			return nickAccount, nickMethod
230
+		case nickMethod == NickReservationNone:
231
+			return skelAccount, skelMethod
232
+		default:
233
+			// nobody can use this nick
234
+			return "!", NickReservationStrict
235
+		}
202 236
 	}
203
-	return
204 237
 }
205 238
 
206 239
 // Looks up the enforcement method stored in the database for an account
@@ -264,10 +297,15 @@ func (am *AccountManager) AccountToClients(account string) (result []*Client) {
264 297
 
265 298
 func (am *AccountManager) Register(client *Client, account string, callbackNamespace string, callbackValue string, passphrase string, certfp string) error {
266 299
 	casefoldedAccount, err := CasefoldName(account)
267
-	if err != nil || account == "" || account == "*" {
300
+	skeleton, skerr := Skeleton(account)
301
+	if err != nil || skerr != nil || account == "" || account == "*" {
268 302
 		return errAccountCreation
269 303
 	}
270 304
 
305
+	if restrictedNicknames[casefoldedAccount] || restrictedNicknames[skeleton] {
306
+		return errAccountAlreadyRegistered
307
+	}
308
+
271 309
 	// can't register a guest nickname
272 310
 	config := am.server.AccountConfig()
273 311
 	renamePrefix := strings.ToLower(config.NickReservation.RenamePrefix)
@@ -535,8 +573,10 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
535 573
 		})
536 574
 
537 575
 		if err == nil {
576
+			skeleton, _ := Skeleton(raw.Name)
538 577
 			am.Lock()
539 578
 			am.nickToAccount[casefoldedAccount] = casefoldedAccount
579
+			am.skeletonToAccount[skeleton] = casefoldedAccount
540 580
 			am.Unlock()
541 581
 		}
542 582
 	}()
@@ -567,9 +607,10 @@ func unmarshalReservedNicks(nicks string) (result []string) {
567 607
 
568 608
 func (am *AccountManager) SetNickReserved(client *Client, nick string, saUnreserve bool, reserve bool) error {
569 609
 	cfnick, err := CasefoldName(nick)
610
+	skeleton, skerr := Skeleton(nick)
570 611
 	// garbage nick, or garbage options, or disabled
571 612
 	nrconfig := am.server.AccountConfig().NickReservation
572
-	if err != nil || cfnick == "" || (reserve && saUnreserve) || !nrconfig.Enabled {
613
+	if err != nil || skerr != nil || cfnick == "" || (reserve && saUnreserve) || !nrconfig.Enabled {
573 614
 		return errAccountNickReservationFailed
574 615
 	}
575 616
 
@@ -591,8 +632,15 @@ func (am *AccountManager) SetNickReserved(client *Client, nick string, saUnreser
591 632
 		return errAccountNotLoggedIn
592 633
 	}
593 634
 
594
-	accountForNick := am.NickToAccount(cfnick)
595
-	if reserve && accountForNick != "" {
635
+	am.Lock()
636
+	accountForNick := am.nickToAccount[cfnick]
637
+	var accountForSkeleton string
638
+	if reserve {
639
+		accountForSkeleton = am.skeletonToAccount[skeleton]
640
+	}
641
+	am.Unlock()
642
+
643
+	if reserve && (accountForNick != "" || accountForSkeleton != "") {
596 644
 		return errNicknameReserved
597 645
 	} else if !reserve && !saUnreserve && accountForNick != account {
598 646
 		return errNicknameReserved
@@ -623,12 +671,18 @@ func (am *AccountManager) SetNickReserved(client *Client, nick string, saUnreser
623 671
 			if len(nicks) >= nrconfig.AdditionalNickLimit {
624 672
 				return errAccountTooManyNicks
625 673
 			}
626
-			nicks = append(nicks, cfnick)
674
+			nicks = append(nicks, nick)
627 675
 		} else {
676
+			// compute (original reserved nicks) minus cfnick
628 677
 			var newNicks []string
629 678
 			for _, reservedNick := range nicks {
630
-				if reservedNick != cfnick {
679
+				cfreservednick, _ := CasefoldName(reservedNick)
680
+				if cfreservednick != cfnick {
631 681
 					newNicks = append(newNicks, reservedNick)
682
+				} else {
683
+					// found the original, unfolded version of the nick we're dropping;
684
+					// recompute the true skeleton from it
685
+					skeleton, _ = Skeleton(reservedNick)
632 686
 				}
633 687
 			}
634 688
 			nicks = newNicks
@@ -650,8 +704,10 @@ func (am *AccountManager) SetNickReserved(client *Client, nick string, saUnreser
650 704
 	defer am.Unlock()
651 705
 	if reserve {
652 706
 		am.nickToAccount[cfnick] = account
707
+		am.skeletonToAccount[skeleton] = account
653 708
 	} else {
654 709
 		delete(am.nickToAccount, cfnick)
710
+		delete(am.skeletonToAccount, skeleton)
655 711
 	}
656 712
 	return nil
657 713
 }
@@ -787,8 +843,10 @@ func (am *AccountManager) Unregister(account string) error {
787 843
 	am.serialCacheUpdateMutex.Lock()
788 844
 	defer am.serialCacheUpdateMutex.Unlock()
789 845
 
846
+	var accountName string
790 847
 	am.server.store.Update(func(tx *buntdb.Tx) error {
791 848
 		tx.Delete(accountKey)
849
+		accountName, _ = tx.Get(accountNameKey)
792 850
 		tx.Delete(accountNameKey)
793 851
 		tx.Delete(verifiedKey)
794 852
 		tx.Delete(registeredTimeKey)
@@ -817,6 +875,7 @@ func (am *AccountManager) Unregister(account string) error {
817 875
 		}
818 876
 	}
819 877
 
878
+	skeleton, _ := Skeleton(accountName)
820 879
 	additionalNicks := unmarshalReservedNicks(rawNicks)
821 880
 
822 881
 	am.Lock()
@@ -825,8 +884,11 @@ func (am *AccountManager) Unregister(account string) error {
825 884
 	clients = am.accountToClients[casefoldedAccount]
826 885
 	delete(am.accountToClients, casefoldedAccount)
827 886
 	delete(am.nickToAccount, casefoldedAccount)
887
+	delete(am.skeletonToAccount, skeleton)
828 888
 	for _, nick := range additionalNicks {
829 889
 		delete(am.nickToAccount, nick)
890
+		additionalSkel, _ := Skeleton(nick)
891
+		delete(am.skeletonToAccount, additionalSkel)
830 892
 	}
831 893
 	for _, client := range clients {
832 894
 		am.logoutOfAccount(client)

+ 20
- 18
irc/client.go View File

@@ -95,6 +95,7 @@ type Client struct {
95 95
 	saslMechanism      string
96 96
 	saslValue          string
97 97
 	server             *Server
98
+	skeleton           string
98 99
 	socket             *Socket
99 100
 	stateMutex         sync.RWMutex // tier 1
100 101
 	username           string
@@ -381,7 +382,7 @@ func (client *Client) Register() {
381 382
 	client.TryResume()
382 383
 
383 384
 	// finish registration
384
-	client.updateNickMask("")
385
+	client.updateNickMask()
385 386
 	client.server.monitorManager.AlertAbout(client, true)
386 387
 }
387 388
 
@@ -565,6 +566,7 @@ func (client *Client) copyResumeData(oldClient *Client) {
565 566
 	vhost := oldClient.vhost
566 567
 	account := oldClient.account
567 568
 	accountName := oldClient.accountName
569
+	skeleton := oldClient.skeleton
568 570
 	oldClient.stateMutex.RUnlock()
569 571
 
570 572
 	// copy all flags, *except* TLS (in the case that the admins enabled
@@ -586,6 +588,7 @@ func (client *Client) copyResumeData(oldClient *Client) {
586 588
 	client.vhost = vhost
587 589
 	client.account = account
588 590
 	client.accountName = accountName
591
+	client.skeleton = skeleton
589 592
 	client.updateNickMaskNoMutex()
590 593
 }
591 594
 
@@ -696,6 +699,14 @@ func (client *Client) Friends(capabs ...caps.Capability) ClientSet {
696 699
 	return friends
697 700
 }
698 701
 
702
+func (client *Client) SetOper(oper *Oper) {
703
+	client.stateMutex.Lock()
704
+	defer client.stateMutex.Unlock()
705
+	client.oper = oper
706
+	// operators typically get a vhost, update the nickmask
707
+	client.updateNickMaskNoMutex()
708
+}
709
+
699 710
 // XXX: CHGHOST requires prefix nickmask to have original hostname,
700 711
 // this is annoying to do correctly
701 712
 func (client *Client) sendChghost(oldNickMask string, vhost string) {
@@ -730,32 +741,23 @@ func (client *Client) SetVHost(vhost string) (updated bool) {
730 741
 }
731 742
 
732 743
 // updateNick updates `nick` and `nickCasefolded`.
733
-func (client *Client) updateNick(nick string) {
734
-	casefoldedName, err := CasefoldName(nick)
735
-	if err != nil {
736
-		client.server.logger.Error("internal", "nick couldn't be casefolded", nick, err.Error())
737
-		return
738
-	}
744
+func (client *Client) updateNick(nick, nickCasefolded, skeleton string) {
739 745
 	client.stateMutex.Lock()
746
+	defer client.stateMutex.Unlock()
740 747
 	client.nick = nick
741
-	client.nickCasefolded = casefoldedName
742
-	client.stateMutex.Unlock()
748
+	client.nickCasefolded = nickCasefolded
749
+	client.skeleton = skeleton
750
+	client.updateNickMaskNoMutex()
743 751
 }
744 752
 
745
-// updateNickMask updates the casefolded nickname and nickmask.
746
-func (client *Client) updateNickMask(nick string) {
747
-	// on "", just regenerate the nickmask etc.
748
-	// otherwise, update the actual nick
749
-	if nick != "" {
750
-		client.updateNick(nick)
751
-	}
752
-
753
+// updateNickMask updates the nickmask.
754
+func (client *Client) updateNickMask() {
753 755
 	client.stateMutex.Lock()
754 756
 	defer client.stateMutex.Unlock()
755 757
 	client.updateNickMaskNoMutex()
756 758
 }
757 759
 
758
-// updateNickMask updates the casefolded nickname and nickmask, not acquiring any mutexes.
760
+// updateNickMaskNoMutex updates the casefolded nickname and nickmask, not acquiring any mutexes.
759 761
 func (client *Client) updateNickMaskNoMutex() {
760 762
 	client.hostname = client.getVHostNoMutex()
761 763
 	if client.hostname == "" {

+ 38
- 8
irc/client_lookup_set.go View File

@@ -34,12 +34,14 @@ func ExpandUserHost(userhost string) (expanded string) {
34 34
 type ClientManager struct {
35 35
 	sync.RWMutex // tier 2
36 36
 	byNick       map[string]*Client
37
+	bySkeleton   map[string]*Client
37 38
 }
38 39
 
39 40
 // NewClientManager returns a new ClientManager.
40 41
 func NewClientManager() *ClientManager {
41 42
 	return &ClientManager{
42
-		byNick: make(map[string]*Client),
43
+		byNick:     make(map[string]*Client),
44
+		bySkeleton: make(map[string]*Client),
43 45
 	}
44 46
 }
45 47
 
@@ -65,7 +67,11 @@ func (clients *ClientManager) Get(nick string) *Client {
65 67
 
66 68
 func (clients *ClientManager) removeInternal(client *Client) (err error) {
67 69
 	// requires holding the writable Lock()
68
-	oldcfnick := client.NickCasefolded()
70
+	oldcfnick, oldskeleton := client.uniqueIdentifiers()
71
+	if oldcfnick == "*" || oldcfnick == "" {
72
+		return errNickMissing
73
+	}
74
+
69 75
 	currentEntry, present := clients.byNick[oldcfnick]
70 76
 	if present {
71 77
 		if currentEntry == client {
@@ -75,7 +81,22 @@ func (clients *ClientManager) removeInternal(client *Client) (err error) {
75 81
 			client.server.logger.Warning("internal", "clients for nick out of sync", oldcfnick)
76 82
 			err = errNickMissing
77 83
 		}
84
+	} else {
85
+		err = errNickMissing
86
+	}
87
+
88
+	currentEntry, present = clients.bySkeleton[oldskeleton]
89
+	if present {
90
+		if currentEntry == client {
91
+			delete(clients.bySkeleton, oldskeleton)
92
+		} else {
93
+			client.server.logger.Warning("internal", "clients for skeleton out of sync", oldskeleton)
94
+			err = errNickMissing
95
+		}
96
+	} else {
97
+		err = errNickMissing
78 98
 	}
99
+
79 100
 	return
80 101
 }
81 102
 
@@ -84,9 +105,6 @@ func (clients *ClientManager) Remove(client *Client) error {
84 105
 	clients.Lock()
85 106
 	defer clients.Unlock()
86 107
 
87
-	if !client.HasNick() {
88
-		return errNickMissing
89
-	}
90 108
 	return clients.removeInternal(client)
91 109
 }
92 110
 
@@ -105,7 +123,9 @@ func (clients *ClientManager) Resume(newClient, oldClient *Client) (err error) {
105 123
 	}
106 124
 	// nick has been reclaimed, grant it to the new client
107 125
 	clients.removeInternal(newClient)
108
-	clients.byNick[oldClient.NickCasefolded()] = newClient
126
+	oldcfnick, oldskeleton := oldClient.uniqueIdentifiers()
127
+	clients.byNick[oldcfnick] = newClient
128
+	clients.bySkeleton[oldskeleton] = newClient
109 129
 
110 130
 	newClient.copyResumeData(oldClient)
111 131
 
@@ -118,8 +138,12 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
118 138
 	if err != nil {
119 139
 		return err
120 140
 	}
141
+	newSkeleton, err := Skeleton(newNick)
142
+	if err != nil {
143
+		return err
144
+	}
121 145
 
122
-	reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick)
146
+	reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
123 147
 
124 148
 	clients.Lock()
125 149
 	defer clients.Unlock()
@@ -129,12 +153,18 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
129 153
 	if currentNewEntry != nil && currentNewEntry != client {
130 154
 		return errNicknameInUse
131 155
 	}
156
+	// analogous checks for skeletons
157
+	skeletonHolder := clients.bySkeleton[newSkeleton]
158
+	if skeletonHolder != nil && skeletonHolder != client {
159
+		return errNicknameInUse
160
+	}
132 161
 	if method == NickReservationStrict && reservedAccount != "" && reservedAccount != client.Account() {
133 162
 		return errNicknameReserved
134 163
 	}
135 164
 	clients.removeInternal(client)
136 165
 	clients.byNick[newcfnick] = client
137
-	client.updateNickMask(newNick)
166
+	clients.bySkeleton[newSkeleton] = client
167
+	client.updateNick(newNick, newcfnick, newSkeleton)
138 168
 	return nil
139 169
 }
140 170
 

+ 9
- 6
irc/getters.go View File

@@ -108,6 +108,15 @@ func (client *Client) Realname() string {
108 108
 	return client.realname
109 109
 }
110 110
 
111
+// uniqueIdentifiers returns the strings for which the server enforces per-client
112
+// uniqueness/ownership; no two clients can have colliding casefolded nicks or
113
+// skeletons.
114
+func (client *Client) uniqueIdentifiers() (nickCasefolded string, skeleton string) {
115
+	client.stateMutex.RLock()
116
+	defer client.stateMutex.RUnlock()
117
+	return client.nickCasefolded, client.skeleton
118
+}
119
+
111 120
 func (client *Client) ResumeToken() string {
112 121
 	client.stateMutex.RLock()
113 122
 	defer client.stateMutex.RUnlock()
@@ -120,12 +129,6 @@ func (client *Client) Oper() *Oper {
120 129
 	return client.oper
121 130
 }
122 131
 
123
-func (client *Client) SetOper(oper *Oper) {
124
-	client.stateMutex.Lock()
125
-	defer client.stateMutex.Unlock()
126
-	client.oper = oper
127
-}
128
-
129 132
 func (client *Client) Registered() bool {
130 133
 	client.stateMutex.RLock()
131 134
 	defer client.stateMutex.RUnlock()

+ 0
- 1
irc/handlers.go View File

@@ -1715,7 +1715,6 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
1715 1715
 
1716 1716
 	oldNickmask := client.NickMaskString()
1717 1717
 	client.SetOper(oper)
1718
-	client.updateNickMask("")
1719 1718
 	if client.NickMaskString() != oldNickmask {
1720 1719
 		client.sendChghost(oldNickmask, oper.Vhost)
1721 1720
 	}

+ 3
- 3
irc/idletimer.go View File

@@ -205,9 +205,9 @@ func (nt *NickTimer) Touch() {
205 205
 		return
206 206
 	}
207 207
 
208
-	nick := nt.client.NickCasefolded()
208
+	cfnick, skeleton := nt.client.uniqueIdentifiers()
209 209
 	account := nt.client.Account()
210
-	accountForNick, method := nt.client.server.accounts.EnforcementStatus(nick)
210
+	accountForNick, method := nt.client.server.accounts.EnforcementStatus(cfnick, skeleton)
211 211
 	enforceTimeout := method == NickReservationWithTimeout
212 212
 
213 213
 	var shouldWarn bool
@@ -223,7 +223,7 @@ func (nt *NickTimer) Touch() {
223 223
 		// the timer will not reset as long as the squatter is targeting the same account
224 224
 		accountChanged := accountForNick != nt.accountForNick
225 225
 		// change state
226
-		nt.nick = nick
226
+		nt.nick = cfnick
227 227
 		nt.account = account
228 228
 		nt.accountForNick = accountForNick
229 229
 		delinquent := accountForNick != "" && accountForNick != account

+ 1
- 1
irc/nickserv.go View File

@@ -215,7 +215,7 @@ func nsGhostHandler(server *Server, client *Client, command string, params []str
215 215
 }
216 216
 
217 217
 func nsGroupHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
218
-	nick := client.NickCasefolded()
218
+	nick := client.Nick()
219 219
 	err := server.accounts.SetNickReserved(client, nick, false, true)
220 220
 	if err == nil {
221 221
 		nsNotice(rb, fmt.Sprintf(client.t("Successfully grouped nick %s with your account"), nick))

+ 57
- 5
irc/strings.go View File

@@ -8,21 +8,25 @@ package irc
8 8
 import (
9 9
 	"strings"
10 10
 
11
+	"github.com/oragono/confusables"
11 12
 	"golang.org/x/text/secure/precis"
13
+	"golang.org/x/text/unicode/norm"
12 14
 )
13 15
 
14 16
 const (
15 17
 	casemappingName = "rfc8265"
16 18
 )
17 19
 
18
-// Casefold returns a casefolded string, without doing any name or channel character checks.
19
-func Casefold(str string) (string, error) {
20
-	var err error
21
-	oldStr := str
20
+// Each pass of PRECIS casefolding is a composition of idempotent operations,
21
+// but not idempotent itself. Therefore, the spec says "do it four times and hope
22
+// it converges" (lolwtf). Golang's PRECIS implementation has a "repeat" option,
23
+// which provides this functionality, but unfortunately it's not exposed publicly.
24
+func iterateFolding(profile *precis.Profile, oldStr string) (str string, err error) {
25
+	str = oldStr
22 26
 	// follow the stabilizing rules laid out here:
23 27
 	// https://tools.ietf.org/html/draft-ietf-precis-7564bis-10.html#section-7
24 28
 	for i := 0; i < 4; i++ {
25
-		str, err = precis.UsernameCaseMapped.CompareKey(str)
29
+		str, err = profile.CompareKey(str)
26 30
 		if err != nil {
27 31
 			return "", err
28 32
 		}
@@ -37,6 +41,11 @@ func Casefold(str string) (string, error) {
37 41
 	return str, nil
38 42
 }
39 43
 
44
+// Casefold returns a casefolded string, without doing any name or channel character checks.
45
+func Casefold(str string) (string, error) {
46
+	return iterateFolding(precis.UsernameCaseMapped, str)
47
+}
48
+
40 49
 // CasefoldChannel returns a casefolded version of a channel name.
41 50
 func CasefoldChannel(name string) (string, error) {
42 51
 	if len(name) == 0 {
@@ -96,3 +105,46 @@ func CasefoldName(name string) (string, error) {
96 105
 
97 106
 	return lowered, err
98 107
 }
108
+
109
+// "boring" names are exempt from skeletonization.
110
+// this is because confusables.txt considers various pure ASCII alphanumeric
111
+// strings confusable: 0 and O, 1 and l, m and rn. IMO this causes more problems
112
+// than it solves.
113
+func isBoring(name string) bool {
114
+	for i := 0; i < len(name); i += 1 {
115
+		chr := name[i]
116
+		if (chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z') || (chr >= '0' && chr <= '9') {
117
+			continue // alphanumerics
118
+		}
119
+		switch chr {
120
+		case '$', '%', '^', '&', '(', ')', '{', '}', '[', ']', '<', '>', '=':
121
+			continue // benign printable ascii characters
122
+		default:
123
+			return false // potentially confusable ascii like | ' `, non-ascii
124
+		}
125
+	}
126
+	return true
127
+}
128
+
129
+var skeletonCasefolder = precis.NewIdentifier(precis.FoldWidth, precis.LowerCase(), precis.Norm(norm.NFC))
130
+
131
+// similar to Casefold, but exempts the bidi rule, because skeletons may
132
+// mix scripts strangely
133
+func casefoldSkeleton(str string) (string, error) {
134
+	return iterateFolding(skeletonCasefolder, str)
135
+}
136
+
137
+// Skeleton produces a canonicalized identifier that tries to catch
138
+// homoglyphic / confusable identifiers. It's a tweaked version of the TR39
139
+// skeleton algorithm. We apply the skeleton algorithm first and only then casefold,
140
+// because casefolding first would lose some information about visual confusability.
141
+// This has the weird consequence that the skeleton is not a function of the
142
+// casefolded identifier --- therefore it must always be computed
143
+// from the original (unfolded) identifier and stored/tracked separately from the
144
+// casefolded identifier.
145
+func Skeleton(name string) (string, error) {
146
+	if !isBoring(name) {
147
+		name = confusables.Skeleton(name)
148
+	}
149
+	return casefoldSkeleton(name)
150
+}

+ 47
- 0
irc/strings_test.go View File

@@ -127,3 +127,50 @@ func TestCasefoldName(t *testing.T) {
127 127
 		})
128 128
 	}
129 129
 }
130
+
131
+func TestIsBoring(t *testing.T) {
132
+	assertBoring := func(str string, expected bool) {
133
+		if isBoring(str) != expected {
134
+			t.Errorf("expected [%s] to have boringness [%t], but got [%t]", str, expected, !expected)
135
+		}
136
+	}
137
+
138
+	assertBoring("warning", true)
139
+	assertBoring("phi|ip", false)
140
+	assertBoring("Νικηφόρος", false)
141
+}
142
+
143
+func TestSkeleton(t *testing.T) {
144
+	skeleton := func(str string) string {
145
+		skel, err := Skeleton(str)
146
+		if err != nil {
147
+			t.Error(err)
148
+		}
149
+		return skel
150
+	}
151
+
152
+	if skeleton("warning") == skeleton("waming") {
153
+		t.Errorf("Oragono shouldn't consider rn confusable with m")
154
+	}
155
+
156
+	if skeleton("Phi|ip") != "philip" {
157
+		t.Errorf("but we still consider pipe confusable with l")
158
+	}
159
+
160
+	if skeleton("smt") != "smt" {
161
+		t.Errorf("fullwidth characters should skeletonize to plain old ascii characters")
162
+	}
163
+
164
+	if skeleton("SMT") != "smt" {
165
+		t.Errorf("after skeletonizing, we should casefold")
166
+	}
167
+
168
+	if skeleton("еvan") != "evan" {
169
+		t.Errorf("we must protect against cyrillic homoglyph attacks")
170
+	}
171
+
172
+	if skeleton("РОТАТО") != "potato" {
173
+		t.Errorf("we must protect against cyrillic homoglyph attacks")
174
+	}
175
+
176
+}

+ 1
- 1
vendor

@@ -1 +1 @@
1
-Subproject commit 77ddc3dbc1ec085c73670510a8fece80599741ce
1
+Subproject commit 0d667e5d09fd0a2041154eca9cdd1915b9843453

Loading…
Cancel
Save